diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Base64EncodedHubProtocol.spec.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Base64EncodedHubProtocol.spec.ts new file mode 100644 index 0000000000..7bd728f94c --- /dev/null +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Base64EncodedHubProtocol.spec.ts @@ -0,0 +1,71 @@ +import { Base64EncodedHubProtocol } from "../Microsoft.AspNetCore.SignalR.Client.TS/Base64EncodedHubProtocol" +import { IHubProtocol, HubMessage, ProtocolType } from "../Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol" + +class FakeHubProtocol implements IHubProtocol { + name: "fakehubprotocol"; + type: ProtocolType; + + parseMessages(input: any): HubMessage[] { + let s = ""; + + new Uint8Array(input).forEach((item: any) => { + s += String.fromCharCode(item); + }); + + return JSON.parse(s); + } + + writeMessage(message: HubMessage): any { + let s = JSON.stringify(message); + let payload = new Uint8Array(s.length); + for (let i = 0; i < payload.length; i++) { + payload[i] = s.charCodeAt(i); + } + return payload; + } +} + +describe("Base64EncodedHubProtocol", () => { + ([ + ["ABC", new Error("Invalid payload.")], + ["3:ABC", new Error("Invalid payload.")], + [":;", new Error("Invalid length: ''")], + ["1.0:A;", new Error("Invalid length: '1.0'")], + ["2:A;", new Error("Invalid message size.")], + ["2:ABC;", new Error("Invalid message size.")], + ] as [[string, Error]]).forEach(([payload, expected_error]) => { + it(`should fail to parse '${payload}'`, () => { + expect(() => new Base64EncodedHubProtocol(new FakeHubProtocol()).parseMessages(payload)).toThrow(expected_error); + }); + }); + + ([ + ["2:{};", {}], + ] as [[string, any]]).forEach(([payload, message]) => { + it(`should be able to parse '${payload}'`, () => { + + let globalAny: any = global; + globalAny.atob = function(input: any) { return input }; + + let result = new Base64EncodedHubProtocol(new FakeHubProtocol()).parseMessages(payload); + expect(result).toEqual(message); + + delete globalAny.atob; + }); + }); + + ([ + [{}, "2:{};"], + ] as [[any, string]]).forEach(([message, payload]) => { + it(`should be able to write '${JSON.stringify(message)}'`, () => { + + let globalAny: any = global; + globalAny.btoa = function(input: any) { return input }; + + let result = new Base64EncodedHubProtocol(new FakeHubProtocol()).writeMessage(message); + expect(result).toEqual(payload); + + delete globalAny.btoa; + }); + }); +}); diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Base64EncodedHubProtocol.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Base64EncodedHubProtocol.ts new file mode 100644 index 0000000000..09e8e9d4e5 --- /dev/null +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Base64EncodedHubProtocol.ts @@ -0,0 +1,57 @@ +import { IHubProtocol, HubMessage, ProtocolType } from "./IHubProtocol" + +export class Base64EncodedHubProtocol implements IHubProtocol { + private wrappedProtocol: IHubProtocol; + + constructor(protocol: IHubProtocol) { + this.wrappedProtocol = protocol; + this.name = this.wrappedProtocol.name; + this.type = ProtocolType.Text; + } + + readonly name: string; + readonly type: ProtocolType; + + parseMessages(input: any): HubMessage[] { + // The format of the message is `size:message;` + let pos = input.indexOf(":"); + if (pos == -1 || !input.endsWith(";")) { + throw new Error("Invalid payload."); + } + + let lenStr = input.substring(0, pos); + if (!/^[0-9]+$/.test(lenStr)) { + throw new Error(`Invalid length: '${lenStr}'`); + } + + let messageSize = parseInt(lenStr, 10); + // 2 accounts for ':' after message size and trailing ';' + if (messageSize != input.length - pos - 2) { + throw new Error("Invalid message size."); + } + + let encodedMessage = input.substring(pos + 1, input.length - 1); + + // atob/btoa are browsers APIs but they can be polyfilled. If this becomes problematic we can use + // base64-js module + let s = atob(encodedMessage); + let payload = new Uint8Array(s.length); + for (let i = 0; i < payload.length; i++) { + payload[i] = s.charCodeAt(i); + } + return this.wrappedProtocol.parseMessages(payload.buffer); + } + + writeMessage(message: HubMessage): any { + let payload = new Uint8Array(this.wrappedProtocol.writeMessage(message)); + let s = ""; + for (let i = 0; i < payload.byteLength; i++) { + s += String.fromCharCode(payload[i]); + } + // atob/btoa are browsers APIs but they can be polyfilled. If this becomes problematic we can use + // base64-js module + let encodedMessage = btoa(s); + + return `${encodedMessage.length.toString()}:${encodedMessage};`; + } +} \ No newline at end of file diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Formatters.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Formatters.ts index 940b09f2da..5a65d14d4d 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Formatters.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Formatters.ts @@ -9,7 +9,6 @@ function splitAt(input: string, searchString: string, position: number): [string } export namespace TextMessageFormat { - const InvalidPayloadError = new Error("Invalid text message payload"); const LengthRegex = /^[0-9]+$/; function hasSpace(input: string, offset: number, length: number): boolean { diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts index 38698d7f6b..c7d11921c6 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts @@ -5,6 +5,7 @@ import { Subject, Observable } from "./Observable" import { IHubProtocol, ProtocolType, MessageType, HubMessage, CompletionMessage, ResultMessage, InvocationMessage, NegotiationMessage } from "./IHubProtocol"; import { JsonHubProtocol } from "./JsonHubProtocol"; import { TextMessageFormat } from "./Formatters" +import { Base64EncodedHubProtocol } from "./Base64EncodedHubProtocol" export { TransportType } from "./Transports" export { HttpConnection } from "./HttpConnection" @@ -210,38 +211,3 @@ export class HubConnection { }; } } - -class Base64EncodedHubProtocol implements IHubProtocol { - private wrappedProtocol: IHubProtocol; - - constructor(protocol: IHubProtocol) { - this.wrappedProtocol = protocol; - this.name = this.wrappedProtocol.name; - this.type = ProtocolType.Text; - } - - readonly name: string; - readonly type: ProtocolType; - - parseMessages(input: any): HubMessage[] { - // atob/btoa are browsers APIs but they can be polyfilled. If this becomes problematic we can use - // base64-js module - let s = atob(input); - let payload = new Uint8Array(s.length); - for (let i = 0; i < payload.length; i++) { - payload[i] = s.charCodeAt(i); - } - return this.wrappedProtocol.parseMessages(payload.buffer); - } - - writeMessage(message: HubMessage) { - let payload = new Uint8Array(this.wrappedProtocol.writeMessage(message)); - let s = ""; - for (var i = 0; i < payload.byteLength; i++) { - s += String.fromCharCode(payload[i]); - } - // atob/btoa are browsers APIs but they can be polyfilled. If this becomes problematic we can use - // base64-js module - return btoa(s); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/Base64Encoder.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/Base64Encoder.cs index f35965243e..bd5ae52e7c 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/Base64Encoder.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/Base64Encoder.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.IO; using System.Text; +using Microsoft.AspNetCore.Sockets.Internal.Formatters; namespace Microsoft.AspNetCore.SignalR.Internal.Encoders { @@ -10,12 +12,20 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Encoders { public byte[] Decode(byte[] payload) { - return Convert.FromBase64String(Encoding.UTF8.GetString(payload)); + var buffer = new ReadOnlyBuffer(payload); + TextMessageParser.TryParseMessage(ref buffer, out var message); + + return Convert.FromBase64String(Encoding.UTF8.GetString(message.ToArray())); } public byte[] Encode(byte[] payload) { - return Encoding.UTF8.GetBytes(Convert.ToBase64String(payload)); + var buffer = Encoding.UTF8.GetBytes(Convert.ToBase64String(payload)); + using (var stream = new MemoryStream()) + { + TextMessageFormatter.WriteMessage(buffer, stream); + return stream.ToArray(); + } } } } diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs index 6b57c4473f..4fe8e59052 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Globalization; using System.IO; using System.Text; using System.Threading.Tasks; @@ -339,8 +340,15 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests await connection.ReadSentTextMessageAsync().OrTimeout(); var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout(); + // The message is in the following format `size:payload;` + var parts = invokeMessage.Split(':'); + Assert.Equal(2, parts.Length); + Assert.True(int.TryParse(parts[0], out var payloadSize)); + Assert.Equal(payloadSize, parts[1].Length - 1); + Assert.EndsWith(";", parts[1]); + // this throws if the message is not a valid base64 string - Convert.FromBase64String(invokeMessage); + Convert.FromBase64String(parts[1].Substring(0, payloadSize)); } finally { @@ -364,11 +372,15 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests using (var ms = new MemoryStream()) { new MessagePackHubProtocol().WriteMessage(new InvocationMessage("1", true, "MyMethod", 42), ms); + var invokeMessage = Convert.ToBase64String(ms.ToArray()); - connection.ReceivedMessages.TryWrite(Encoding.UTF8.GetBytes(invokeMessage)); + var payloadSize = invokeMessage.Length.ToString(CultureInfo.InvariantCulture); + var message = $"{payloadSize}:{invokeMessage};"; + + connection.ReceivedMessages.TryWrite(Encoding.UTF8.GetBytes(message)); } - Assert.Equal(42, await invocationTcs.Task); + Assert.Equal(42, await invocationTcs.Task.OrTimeout()); } finally {