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 {
|
export namespace TextMessageFormat {
|
||||||
const InvalidPayloadError = new Error("Invalid text message payload");
|
|
||||||
const LengthRegex = /^[0-9]+$/;
|
const LengthRegex = /^[0-9]+$/;
|
||||||
|
|
||||||
function hasSpace(input: string, offset: number, length: number): boolean {
|
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 { IHubProtocol, ProtocolType, MessageType, HubMessage, CompletionMessage, ResultMessage, InvocationMessage, NegotiationMessage } from "./IHubProtocol";
|
||||||
import { JsonHubProtocol } from "./JsonHubProtocol";
|
import { JsonHubProtocol } from "./JsonHubProtocol";
|
||||||
import { TextMessageFormat } from "./Formatters"
|
import { TextMessageFormat } from "./Formatters"
|
||||||
|
import { Base64EncodedHubProtocol } from "./Base64EncodedHubProtocol"
|
||||||
|
|
||||||
export { TransportType } from "./Transports"
|
export { TransportType } from "./Transports"
|
||||||
export { HttpConnection } from "./HttpConnection"
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Sockets.Internal.Formatters;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
|
namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
|
||||||
{
|
{
|
||||||
|
|
@ -10,12 +12,20 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Encoders
|
||||||
{
|
{
|
||||||
public byte[] Decode(byte[] payload)
|
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)
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -339,8 +340,15 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
await connection.ReadSentTextMessageAsync().OrTimeout();
|
await connection.ReadSentTextMessageAsync().OrTimeout();
|
||||||
var invokeMessage = 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
|
// this throws if the message is not a valid base64 string
|
||||||
Convert.FromBase64String(invokeMessage);
|
Convert.FromBase64String(parts[1].Substring(0, payloadSize));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -364,11 +372,15 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
using (var ms = new MemoryStream())
|
using (var ms = new MemoryStream())
|
||||||
{
|
{
|
||||||
new MessagePackHubProtocol().WriteMessage(new InvocationMessage("1", true, "MyMethod", 42), ms);
|
new MessagePackHubProtocol().WriteMessage(new InvocationMessage("1", true, "MyMethod", 42), ms);
|
||||||
|
|
||||||
var invokeMessage = Convert.ToBase64String(ms.ToArray());
|
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
|
finally
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue