diff --git a/src/WebJobs.Script/Rpc/LanguageWorkerChannel.cs b/src/WebJobs.Script/Rpc/LanguageWorkerChannel.cs index e7e210de26..85f0ba3154 100644 --- a/src/WebJobs.Script/Rpc/LanguageWorkerChannel.cs +++ b/src/WebJobs.Script/Rpc/LanguageWorkerChannel.cs @@ -455,7 +455,7 @@ internal void SendInvocationRequest(ScriptInvocationContext context) { if (pair.Value != null) { - invocationRequest.TriggerMetadata.Add(pair.Key, pair.Value.ToRpc(_workerChannelLogger)); + invocationRequest.TriggerMetadata.Add(pair.Key, pair.Value.ToRpc(_workerChannelLogger, _workerCapabilities)); } } foreach (var input in context.Inputs) @@ -463,7 +463,7 @@ internal void SendInvocationRequest(ScriptInvocationContext context) invocationRequest.InputData.Add(new ParameterBinding() { Name = input.name, - Data = input.val.ToRpc(_workerChannelLogger) + Data = input.val.ToRpc(_workerChannelLogger, _workerCapabilities) }); } diff --git a/src/WebJobs.Script/Rpc/LanguageWorkerConstants.cs b/src/WebJobs.Script/Rpc/LanguageWorkerConstants.cs index f2b59d0d33..0915e9b2c7 100644 --- a/src/WebJobs.Script/Rpc/LanguageWorkerConstants.cs +++ b/src/WebJobs.Script/Rpc/LanguageWorkerConstants.cs @@ -47,5 +47,8 @@ public static class LanguageWorkerConstants public const string RpcHttpCookies = "cookies"; public const string RpcHttpStatusCode = "statusCode"; public const string RpcHttpStatus = "status"; + + // Capabilites + public const string RawHttpBodyBytes = "RawHttpBodyBytes"; } } \ No newline at end of file diff --git a/src/WebJobs.Script/Rpc/MessageExtensions/RpcMessageConversionExtensions.cs b/src/WebJobs.Script/Rpc/MessageExtensions/RpcMessageConversionExtensions.cs index dabad4821a..e07e286990 100644 --- a/src/WebJobs.Script/Rpc/MessageExtensions/RpcMessageConversionExtensions.cs +++ b/src/WebJobs.Script/Rpc/MessageExtensions/RpcMessageConversionExtensions.cs @@ -47,7 +47,7 @@ public static object ToObject(this TypedData typedData) } } - public static TypedData ToRpc(this object value, ILogger logger) + public static TypedData ToRpc(this object value, ILogger logger, Capabilities capabilities) { TypedData typedData = new TypedData(); @@ -141,7 +141,8 @@ public static TypedData ToRpc(this object value, ILogger logger) if (request.Body != null && request.ContentLength > 0) { object body = null; - string rawBody = null; + string rawBodyString = null; + byte[] bytes = RequestBodyToBytes(request); MediaTypeHeaderValue mediaType = null; if (MediaTypeHeaderValue.TryParse(request.ContentType, out mediaType)) @@ -149,36 +150,43 @@ public static TypedData ToRpc(this object value, ILogger logger) if (string.Equals(mediaType.MediaType, "application/json", StringComparison.OrdinalIgnoreCase)) { var jsonReader = new StreamReader(request.Body, Encoding.UTF8); - rawBody = jsonReader.ReadToEnd(); + rawBodyString = jsonReader.ReadToEnd(); try { - body = JsonConvert.DeserializeObject(rawBody); + body = JsonConvert.DeserializeObject(rawBodyString); } catch (JsonException) { - body = rawBody; + body = rawBodyString; } } else if (string.Equals(mediaType.MediaType, "application/octet-stream", StringComparison.OrdinalIgnoreCase) || mediaType.MediaType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0) { - var length = Convert.ToInt32(request.ContentLength); - var bytes = new byte[length]; - request.Body.Read(bytes, 0, length); body = bytes; - rawBody = Encoding.UTF8.GetString(bytes); + if (!IsRawBodyBytesRequested(capabilities)) + { + rawBodyString = Encoding.UTF8.GetString(bytes); + } } } // default if content-tye not found or recognized - if (body == null && rawBody == null) + if (body == null && rawBodyString == null) { var reader = new StreamReader(request.Body, Encoding.UTF8); - body = rawBody = reader.ReadToEnd(); + body = rawBodyString = reader.ReadToEnd(); } request.Body.Position = 0; - http.Body = body.ToRpc(logger); - http.RawBody = rawBody.ToRpc(logger); + http.Body = body.ToRpc(logger, capabilities); + if (IsRawBodyBytesRequested(capabilities)) + { + http.RawBody = bytes.ToRpc(logger, capabilities); + } + else + { + http.RawBody = rawBodyString.ToRpc(logger, capabilities); + } } } else @@ -196,6 +204,20 @@ public static TypedData ToRpc(this object value, ILogger logger) return typedData; } + private static bool IsRawBodyBytesRequested(Capabilities capabilities) + { + return capabilities.GetCapabilityState(LanguageWorkerConstants.RawHttpBodyBytes) != null; + } + + internal static byte[] RequestBodyToBytes(HttpRequest request) + { + var length = Convert.ToInt32(request.ContentLength); + var bytes = new byte[length]; + request.Body.Read(bytes, 0, length); + request.Body.Position = 0; + return bytes; + } + public static BindingInfo ToBindingInfo(this BindingMetadata bindingMetadata) { BindingInfo bindingInfo = new BindingInfo diff --git a/test/WebJobs.Script.Tests/Rpc/Resources/functions.png b/test/WebJobs.Script.Tests/Rpc/Resources/functions.png new file mode 100644 index 0000000000..42ea4bbf5c Binary files /dev/null and b/test/WebJobs.Script.Tests/Rpc/Resources/functions.png differ diff --git a/test/WebJobs.Script.Tests/Rpc/RpcMessageConversionExtensionsTests.cs b/test/WebJobs.Script.Tests/Rpc/RpcMessageConversionExtensionsTests.cs index 8f251054bb..bbb4711ed8 100644 --- a/test/WebJobs.Script.Tests/Rpc/RpcMessageConversionExtensionsTests.cs +++ b/test/WebJobs.Script.Tests/Rpc/RpcMessageConversionExtensionsTests.cs @@ -3,8 +3,11 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Reflection; using System.Security.Claims; +using System.Text; using Google.Protobuf.Collections; using Google.Protobuf.WellKnownTypes; using Microsoft.AspNetCore.Http; @@ -18,17 +21,20 @@ namespace Microsoft.Azure.WebJobs.Script.Tests.Rpc { public class RpcMessageConversionExtensionsTests { + private static readonly string TestImageLocation = "Rpc\\Resources\\functions.png"; + [Theory] [InlineData("application/x-www-form-urlencoded’", "say=Hi&to=Mom")] public void HttpObjects_StringBody(string expectedContentType, object body) { var logger = MockNullLoggerFactory.CreateLogger(); + var capabilities = new Capabilities(logger); var headers = new HeaderDictionary(); headers.Add("content-type", expectedContentType); HttpRequest request = HttpTestHelpers.CreateHttpRequest("GET", "http://localhost/api/httptrigger-scenarios", headers, body); - var rpcRequestObject = request.ToRpc(logger); + var rpcRequestObject = request.ToRpc(logger, capabilities); Assert.Equal(body.ToString(), rpcRequestObject.Http.Body.String); string contentType; @@ -43,10 +49,11 @@ public void HttpObjects_StringBody(string expectedContentType, object body) public void HttpObjects_Query(string queryString, string[] expectedKeys, string[] expectedValues) { var logger = MockNullLoggerFactory.CreateLogger(); + var capabilities = new Capabilities(logger); HttpRequest request = HttpTestHelpers.CreateHttpRequest("GET", $"http://localhost/api/httptrigger-scenarios?{queryString}"); - var rpcRequestObject = request.ToRpc(logger); + var rpcRequestObject = request.ToRpc(logger, capabilities); // Same number of expected key value pairs Assert.Equal(rpcRequestObject.Http.Query.Count, expectedKeys.Length); Assert.Equal(rpcRequestObject.Http.Query.Count, expectedValues.Length); @@ -188,6 +195,7 @@ public void SetCookie_ReturnsExpectedResult(string name, string value, RpcHttpCo public void HttpObjects_ClaimsPrincipal() { var logger = MockNullLoggerFactory.CreateLogger(); + var capabilities = new Capabilities(logger); HttpRequest request = HttpTestHelpers.CreateHttpRequest("GET", $"http://localhost/apihttptrigger-scenarios"); @@ -201,7 +209,7 @@ public void HttpObjects_ClaimsPrincipal() request.HttpContext.User = new ClaimsPrincipal(claimsIdentities); - var rpcRequestObject = request.ToRpc(logger); + var rpcRequestObject = request.ToRpc(logger, capabilities); var identities = request.HttpContext.User.Identities.ToList(); var rpcIdentities = rpcRequestObject.Http.Identities.ToList(); @@ -247,5 +255,55 @@ internal static ClaimsIdentity MockEasyAuth(string provider, string name, string return identity; } + + [Theory] + [InlineData("application/octet-stream", "true")] + [InlineData("image/png", "true")] + [InlineData("application/octet-stream", null)] + [InlineData("image/png", null)] + public void HttpObjects_RawBodyBytes_Image_Length(string contentType, string rawBytesEnabled) + { + var logger = MockNullLoggerFactory.CreateLogger(); + var capabilities = new Capabilities(logger); + if (!string.Equals(rawBytesEnabled, null)) + { + capabilities.UpdateCapabilities(new MapField + { + { LanguageWorkerConstants.RawHttpBodyBytes, rawBytesEnabled.ToString() } + }); + } + + FileStream image = new FileStream(TestImageLocation, FileMode.Open, FileAccess.Read); + byte[] imageBytes = FileStreamToBytes(image); + string imageString = Encoding.UTF8.GetString(imageBytes); + + long imageBytesLength = imageBytes.Length; + long imageStringLength = imageString.Length; + + var headers = new HeaderDictionary(); + headers.Add("content-type", contentType); + HttpRequest request = HttpTestHelpers.CreateHttpRequest("GET", "http://localhost/api/httptrigger-scenarios", headers, imageBytes); + + var rpcRequestObject = request.ToRpc(logger, capabilities); + if (string.IsNullOrEmpty(rawBytesEnabled)) + { + Assert.Empty(rpcRequestObject.Http.RawBody.Bytes); + Assert.Equal(imageStringLength, rpcRequestObject.Http.RawBody.String.Length); + } + else + { + Assert.Empty(rpcRequestObject.Http.RawBody.String); + Assert.Equal(imageBytesLength, rpcRequestObject.Http.RawBody.Bytes.ToByteArray().Length); + } + } + + private byte[] FileStreamToBytes(FileStream file) + { + using (var memoryStream = new MemoryStream()) + { + file.CopyTo(memoryStream); + return memoryStream.ToArray(); + } + } } } diff --git a/test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj b/test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj index bf9ea322f5..55b50484ae 100644 --- a/test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj +++ b/test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj @@ -88,6 +88,9 @@ Always + + PreserveNewest + PreserveNewest