From 879646aba346342006a805c197dba2f20358db1c Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 19 May 2018 18:43:14 +1200 Subject: [PATCH] Fix losing UTC DateTimeKind on ISO8601 UTC values (#2317) --- src/Common/JsonUtils.cs | 23 +++++++++++++ .../Protocol/JsonHubProtocol.cs | 24 ++++++++++++-- .../Internal/Protocol/JsonHubProtocolTests.cs | 32 +++++++++++++++++++ 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/Common/JsonUtils.cs b/src/Common/JsonUtils.cs index 4c7d6219ad..a5ab92ecec 100644 --- a/src/Common/JsonUtils.cs +++ b/src/Common/JsonUtils.cs @@ -153,6 +153,29 @@ namespace Microsoft.AspNetCore.Internal return true; } + public static bool ReadForType(JsonTextReader reader, Type type) + { + // Explicity read values as dates from JSON with reader. + // We do this because otherwise dates are read as strings + // and the JsonSerializer will use a conversion method that won't + // preserve UTC in DateTime.Kind for UTC ISO8601 dates + if (type == typeof(DateTime) || type == typeof(DateTime?)) + { + reader.ReadAsDateTime(); + } + else if (type == typeof(DateTimeOffset) || type == typeof(DateTimeOffset?)) + { + reader.ReadAsDateTimeOffset(); + } + else + { + reader.Read(); + } + + // TokenType will be None if there is no more content + return reader.TokenType != JsonToken.None; + } + private class JsonArrayPool : IArrayPool { private readonly ArrayPool _inner; diff --git a/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Protocol/JsonHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Protocol/JsonHubProtocol.cs index 225bf6337e..3a616ff1a5 100644 --- a/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Protocol/JsonHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Protocol/JsonHubProtocol.cs @@ -168,12 +168,12 @@ namespace Microsoft.AspNetCore.SignalR.Protocol error = JsonUtils.ReadAsString(reader, ErrorPropertyName); break; case ResultPropertyName: - JsonUtils.CheckRead(reader); - hasResult = true; if (string.IsNullOrEmpty(invocationId)) { + JsonUtils.CheckRead(reader); + // If we don't have an invocation id then we need to store it as a JToken so we can parse it later resultToken = JToken.Load(reader); } @@ -181,6 +181,12 @@ namespace Microsoft.AspNetCore.SignalR.Protocol { // If we have an invocation id already we can parse the end result var returnType = binder.GetReturnType(invocationId); + + if (!JsonUtils.ReadForType(reader, returnType)) + { + throw new JsonReaderException("Unexpected end when reading JSON"); + } + result = PayloadSerializer.Deserialize(reader, returnType); } break; @@ -599,6 +605,18 @@ namespace Microsoft.AspNetCore.SignalR.Protocol return new InvocationMessage(invocationId, target, arguments); } + private bool ReadArgumentAsType(JsonTextReader reader, IReadOnlyList paramTypes, int paramIndex) + { + if (paramIndex < paramTypes.Count) + { + var paramType = paramTypes[paramIndex]; + + return JsonUtils.ReadForType(reader, paramType); + } + + return reader.Read(); + } + private object[] BindArguments(JsonTextReader reader, IReadOnlyList paramTypes) { object[] arguments = null; @@ -606,7 +624,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol var argumentsCount = 0; var paramCount = paramTypes.Count; - while (reader.Read()) + while (ReadArgumentAsType(reader, paramTypes, paramIndex)) { if (reader.TokenType == JsonToken.EndArray) { diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs index de764aed24..2cc9112514 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs @@ -250,6 +250,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol [InlineData("{'type':4,'invocationId':'42','target':'foo','arguments':[ 'abc', 'xyz']}", "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.")] [InlineData("{'type':1,'invocationId':'42','target':'foo','arguments':[1,'',{'1':1,'2':2}]}", "Invocation provides 3 argument(s) but target expects 2.")] [InlineData("{'type':1,'arguments':[1,'',{'1':1,'2':2}]},'invocationId':'42','target':'foo'", "Invocation provides 3 argument(s) but target expects 2.")] + [InlineData("{'type':1,'invocationId':'42','target':'foo','arguments':[1,[]]}", "Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.")] public void ArgumentBindingErrors(string input, string expectedMessage) { input = Frame(input); @@ -262,6 +263,37 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol Assert.Equal(expectedMessage, bindingFailure.BindingFailure.SourceException.Message); } + [Theory] + [InlineData("{'type':1,'invocationId':'42','target':'foo','arguments':['2007-03-01T13:00:00Z']}")] + [InlineData("{'type':1,'invocationId':'42','arguments':['2007-03-01T13:00:00Z'],'target':'foo'}")] + public void DateTimeArgumentPreservesUtcKind(string input) + { + var binder = new TestBinder(new[] { typeof(DateTime) }); + var protocol = new JsonHubProtocol(); + var data = new ReadOnlySequence(Encoding.UTF8.GetBytes(Frame(input))); + protocol.TryParseMessage(ref data, binder, out var message); + var invocationMessage = Assert.IsType(message); + + Assert.Single(invocationMessage.Arguments); + var dt = Assert.IsType(invocationMessage.Arguments[0]); + Assert.Equal(DateTimeKind.Utc, dt.Kind); + } + + [Theory] + [InlineData("{'type':3,'invocationId':'42','target':'foo','arguments':[],'result':'2007-03-01T13:00:00Z'}")] + [InlineData("{'type':3,'target':'foo','arguments':[],'result':'2007-03-01T13:00:00Z','invocationId':'42'}")] + public void DateTimeReturnValuePreservesUtcKind(string input) + { + var binder = new TestBinder(typeof(DateTime)); + var protocol = new JsonHubProtocol(); + var data = new ReadOnlySequence(Encoding.UTF8.GetBytes(Frame(input))); + protocol.TryParseMessage(ref data, binder, out var message); + var invocationMessage = Assert.IsType(message); + + var dt = Assert.IsType(invocationMessage.Result); + Assert.Equal(DateTimeKind.Utc, dt.Kind); + } + private static string Frame(string input) { var data = Encoding.UTF8.GetBytes(input);