// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.IO; using Microsoft.AspNetCore.Sockets.Internal.Formatters; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.SignalR.Internal.Protocol { public class JsonHubProtocol : IHubProtocol { private const string ResultPropertyName = "result"; private const string ItemPropertyName = "item"; private const string InvocationIdPropertyName = "invocationId"; private const string TypePropertyName = "type"; private const string ErrorPropertyName = "error"; private const string TargetPropertyName = "target"; private const string NonBlockingPropertyName = "nonBlocking"; private const string ArgumentsPropertyName = "arguments"; private const int InvocationMessageType = 1; private const int ResultMessageType = 2; private const int CompletionMessageType = 3; // ONLY to be used for application payloads (args, return values, etc.) private JsonSerializer _payloadSerializer; /// /// Creates an instance of the using the specified /// to serialize application payloads (arguments, results, etc.). The serialization of the outer protocol can /// NOT be changed using this serializer. /// /// The to use to serialize application payloads (arguments, results, etc.). public JsonHubProtocol(JsonSerializer payloadSerializer) { if (payloadSerializer == null) { throw new ArgumentNullException(nameof(payloadSerializer)); } _payloadSerializer = payloadSerializer; } public bool TryParseMessages(ReadOnlySpan input, IInvocationBinder binder, out IList messages) { messages = new List(); var parser = new TextMessageParser(); while (parser.TryParseMessage(ref input, out var payload)) { // TODO: Need a span-native JSON parser! using (var memoryStream = new MemoryStream(payload.ToArray())) { messages.Add(ParseMessage(memoryStream, binder)); } } return messages.Count > 0; } public bool TryWriteMessage(HubMessage message, Stream output) { using (var memoryStream = new MemoryStream()) { WriteMessage(message, memoryStream); memoryStream.Flush(); return TextMessageFormatter.TryWriteMessage(memoryStream.ToArray(), output); } } private HubMessage ParseMessage(Stream input, IInvocationBinder binder) { using (var reader = new JsonTextReader(new StreamReader(input))) { try { // PERF: Could probably use the JsonTextReader directly for better perf and fewer allocations var token = JToken.ReadFrom(reader); if (token == null) { return null; } if (token.Type != JTokenType.Object) { throw new FormatException($"Unexpected JSON Token Type '{token.Type}'. Expected a JSON Object."); } var json = (JObject)token; // Determine the type of the message var type = GetRequiredProperty(json, TypePropertyName, JTokenType.Integer); switch (type) { case InvocationMessageType: return BindInvocationMessage(json, binder); case ResultMessageType: return BindResultMessage(json, binder); case CompletionMessageType: return BindCompletionMessage(json, binder); default: throw new FormatException($"Unknown message type: {type}"); } } catch (JsonReaderException jrex) { throw new FormatException("Error reading JSON.", jrex); } } } private void WriteMessage(HubMessage message, Stream stream) { using (var writer = new JsonTextWriter(new StreamWriter(stream))) { switch (message) { case InvocationMessage m: WriteInvocationMessage(m, writer); break; case StreamItemMessage m: WriteStreamItemMessage(m, writer); break; case CompletionMessage m: WriteCompletionMessage(m, writer); break; default: throw new InvalidOperationException($"Unsupported message type: {message.GetType().FullName}"); } } } private void WriteCompletionMessage(CompletionMessage message, JsonTextWriter writer) { writer.WriteStartObject(); WriteHubMessageCommon(message, writer, CompletionMessageType); if (!string.IsNullOrEmpty(message.Error)) { writer.WritePropertyName(ErrorPropertyName); writer.WriteValue(message.Error); } else if (message.HasResult) { writer.WritePropertyName(ResultPropertyName); _payloadSerializer.Serialize(writer, message.Result); } writer.WriteEndObject(); } private void WriteStreamItemMessage(StreamItemMessage message, JsonTextWriter writer) { writer.WriteStartObject(); WriteHubMessageCommon(message, writer, ResultMessageType); writer.WritePropertyName(ItemPropertyName); _payloadSerializer.Serialize(writer, message.Item); writer.WriteEndObject(); } private void WriteInvocationMessage(InvocationMessage message, JsonTextWriter writer) { writer.WriteStartObject(); WriteHubMessageCommon(message, writer, InvocationMessageType); writer.WritePropertyName(TargetPropertyName); writer.WriteValue(message.Target); if (message.NonBlocking) { writer.WritePropertyName(NonBlockingPropertyName); writer.WriteValue(message.NonBlocking); } writer.WritePropertyName(ArgumentsPropertyName); writer.WriteStartArray(); foreach (var argument in message.Arguments) { _payloadSerializer.Serialize(writer, argument); } writer.WriteEndArray(); writer.WriteEndObject(); } private static void WriteHubMessageCommon(HubMessage message, JsonTextWriter writer, int type) { writer.WritePropertyName(InvocationIdPropertyName); writer.WriteValue(message.InvocationId); writer.WritePropertyName(TypePropertyName); writer.WriteValue(type); } private InvocationMessage BindInvocationMessage(JObject json, IInvocationBinder binder) { var invocationId = GetRequiredProperty(json, InvocationIdPropertyName, JTokenType.String); var target = GetRequiredProperty(json, TargetPropertyName, JTokenType.String); var nonBlocking = GetOptionalProperty(json, NonBlockingPropertyName, JTokenType.Boolean); var args = GetRequiredProperty(json, ArgumentsPropertyName, JTokenType.Array); var paramTypes = binder.GetParameterTypes(target); var arguments = new object[args.Count]; if (paramTypes.Length != arguments.Length) { throw new FormatException($"Invocation provides {arguments.Length} argument(s) but target expects {paramTypes.Length}."); } for (var i = 0; i < paramTypes.Length; i++) { var paramType = paramTypes[i]; // TODO(anurse): We can add some DI magic here to allow users to provide their own serialization // Related Bug: https://github.com/aspnet/SignalR/issues/261 arguments[i] = args[i].ToObject(paramType, _payloadSerializer); } return new InvocationMessage(invocationId, nonBlocking, target, arguments); } private StreamItemMessage BindResultMessage(JObject json, IInvocationBinder binder) { var invocationId = GetRequiredProperty(json, InvocationIdPropertyName, JTokenType.String); var result = GetRequiredProperty(json, ItemPropertyName); var returnType = binder.GetReturnType(invocationId); return new StreamItemMessage(invocationId, result?.ToObject(returnType, _payloadSerializer)); } private CompletionMessage BindCompletionMessage(JObject json, IInvocationBinder binder) { var invocationId = GetRequiredProperty(json, InvocationIdPropertyName, JTokenType.String); var error = GetOptionalProperty(json, ErrorPropertyName, JTokenType.String); var resultProp = json.Property(ResultPropertyName); if (error != null && resultProp != null) { throw new FormatException("The 'error' and 'result' properties are mutually exclusive."); } if (resultProp == null) { return new CompletionMessage(invocationId, error, result: null, hasResult: false); } else { var returnType = binder.GetReturnType(invocationId); var payload = resultProp.Value?.ToObject(returnType, _payloadSerializer); return new CompletionMessage(invocationId, error, result: payload, hasResult: true); } } private T GetOptionalProperty(JObject json, string property, JTokenType expectedType = JTokenType.None, T defaultValue = default(T)) { var prop = json[property]; if (prop == null) { return defaultValue; } return GetValue(property, expectedType, prop); } private T GetRequiredProperty(JObject json, string property, JTokenType expectedType = JTokenType.None) { var prop = json[property]; if (prop == null) { throw new FormatException($"Missing required property '{property}'."); } return GetValue(property, expectedType, prop); } private static T GetValue(string property, JTokenType expectedType, JToken prop) { if (expectedType != JTokenType.None && prop.Type != expectedType) { throw new FormatException($"Expected '{property}' to be of type {expectedType}."); } return prop.Value(); } } }