Length prefixing base64 encoded messages

... in preparation for pipeline conversion
This commit is contained in:
Pawel Kadluczka 2017-08-10 16:41:19 -07:00 committed by Pawel Kadluczka
parent ec09268698
commit a359da0c44
6 changed files with 156 additions and 41 deletions

View File

@ -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;
});
});
});

View File

@ -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};`;
}
}

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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<byte>(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();
}
}
}
}

View File

@ -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
{