Length prefixing base64 encoded messages
... in preparation for pipeline conversion
This commit is contained in:
parent
ec09268698
commit
a359da0c44
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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};`;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue