From 3fa10f92adf8be2c3c390ed78251143f82c0745c Mon Sep 17 00:00:00 2001 From: Andrew Stanton-Nurse Date: Tue, 29 May 2018 11:27:29 -0700 Subject: [PATCH] Fix losing UTC DateTimeKind on ISO8601 UTC values (#2317) (#2357) --- src/Common/JsonUtils.cs | 23 ++++++++++++++ .../Protocol/JsonHubProtocol.cs | 24 ++++++++++++-- .../Internal/Protocol/JsonHubProtocolTests.cs | 31 +++++++++++++++++++ 3 files changed, 75 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 e2464bbbe4..423fb60555 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,13 +605,25 @@ 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; var paramIndex = 0; var argumentsCount = 0; - 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 c30209ccce..db6ed25702 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs @@ -260,6 +260,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);