Add protocol version to handshake (#1666)
This commit is contained in:
parent
6d642ea5ce
commit
a47e1051e8
|
|
@ -50,8 +50,14 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
|||
public class FakeHubProtocol : IHubProtocol
|
||||
{
|
||||
public string Name { get; }
|
||||
public int Version => 1;
|
||||
public TransferFormat TransferFormat { get; }
|
||||
|
||||
public bool IsVersionSupported(int version)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
||||
{
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
"build:cjs": "node ../node_modules/typescript/bin/tsc --project ./tsconfig.json --module commonjs --outDir ./dist/cjs --target ES5",
|
||||
"build:browser": "node ../node_modules/rollup/bin/rollup -c",
|
||||
"build:uglify": "node ../node_modules/uglify-js/bin/uglifyjs --source-map \"url='signalr-protocol-msgpack.min.js.map',content='./dist/browser/signalr-protocol-msgpack.js.map'\" --comments -o ./dist/browser/signalr-protocol-msgpack.min.js ./dist/browser/signalr-protocol-msgpack.js",
|
||||
"pretest": "node ../node_modules/rimraf/bin.js ./spec/obj && node ../node_modules/typescript/bin/tsc --project ./spec/tsconfig.json && cd ./spec/obj/src && npm init -y && npm install ../../../../signalr",
|
||||
"pretest": "node ../node_modules/rimraf/bin.js ./spec/obj && node ../node_modules/typescript/bin/tsc --project ./spec/tsconfig.json && cd ./spec/obj && npm init -y && npm install ../../../signalr",
|
||||
"test": "node ../node_modules/jasmine/bin/jasmine.js ./spec/obj/spec/**/*.spec.js"
|
||||
},
|
||||
"keywords": [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// 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.
|
||||
|
||||
import { CompletionMessage, InvocationMessage, MessageType, StreamItemMessage } from "@aspnet/signalr";
|
||||
import { CompletionMessage, InvocationMessage, MessageType, NullLogger, StreamItemMessage } from "@aspnet/signalr";
|
||||
import { MessagePackHubProtocol } from "../src/MessagePackHubProtocol";
|
||||
|
||||
describe("MessageHubProtocol", () => {
|
||||
|
|
@ -14,7 +14,7 @@ describe("MessageHubProtocol", () => {
|
|||
} as InvocationMessage;
|
||||
|
||||
const protocol = new MessagePackHubProtocol();
|
||||
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation));
|
||||
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), new NullLogger());
|
||||
expect(parsedMessages).toEqual([invocation]);
|
||||
});
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ describe("MessageHubProtocol", () => {
|
|||
} as InvocationMessage;
|
||||
|
||||
const protocol = new MessagePackHubProtocol();
|
||||
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation));
|
||||
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), new NullLogger());
|
||||
expect(parsedMessages).toEqual([invocation]);
|
||||
});
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ describe("MessageHubProtocol", () => {
|
|||
} as InvocationMessage;
|
||||
|
||||
const protocol = new MessagePackHubProtocol();
|
||||
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation));
|
||||
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), new NullLogger());
|
||||
expect(parsedMessages).toEqual([invocation]);
|
||||
});
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ describe("MessageHubProtocol", () => {
|
|||
} as InvocationMessage;
|
||||
|
||||
const protocol = new MessagePackHubProtocol();
|
||||
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation));
|
||||
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), new NullLogger());
|
||||
expect(parsedMessages).toEqual([invocation]);
|
||||
});
|
||||
|
||||
|
|
@ -93,9 +93,18 @@ describe("MessageHubProtocol", () => {
|
|||
result: new Date(Date.UTC(2018, 0, 1, 11, 24, 0)),
|
||||
type: MessageType.Completion,
|
||||
} as CompletionMessage],
|
||||
// extra property at the end should be ignored (testing older protocol client working with newer protocol server)
|
||||
[[0x09, 0x95, 0x03, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x02, 0x00],
|
||||
{
|
||||
error: null,
|
||||
headers: {},
|
||||
invocationId: "abc",
|
||||
result: null,
|
||||
type: MessageType.Completion,
|
||||
} as CompletionMessage],
|
||||
] as Array<[number[], CompletionMessage]>).forEach(([payload, expectedMessage]) =>
|
||||
it("can read Completion message", () => {
|
||||
const messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer);
|
||||
const messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer, new NullLogger());
|
||||
expect(messages).toEqual([expectedMessage]);
|
||||
}));
|
||||
|
||||
|
|
@ -113,10 +122,10 @@ describe("MessageHubProtocol", () => {
|
|||
invocationId: "abc",
|
||||
item: new Date(Date.UTC(2018, 0, 1, 11, 24, 0)),
|
||||
type: MessageType.StreamItem,
|
||||
} as StreamItemMessage]
|
||||
} as StreamItemMessage],
|
||||
] as Array<[number[], StreamItemMessage]>).forEach(([payload, expectedMessage]) =>
|
||||
it("can read StreamItem message", () => {
|
||||
const messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer);
|
||||
const messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer, new NullLogger());
|
||||
expect(messages).toEqual([expectedMessage]);
|
||||
}));
|
||||
|
||||
|
|
@ -132,7 +141,7 @@ describe("MessageHubProtocol", () => {
|
|||
} as StreamItemMessage],
|
||||
] as Array<[number[], StreamItemMessage]>).forEach(([payload, expectedMessage]) =>
|
||||
it("can read message with headers", () => {
|
||||
const messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer);
|
||||
const messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer, new NullLogger());
|
||||
expect(messages).toEqual([expectedMessage]);
|
||||
}));
|
||||
|
||||
|
|
@ -140,18 +149,15 @@ describe("MessageHubProtocol", () => {
|
|||
["message with no payload", [0x00], new Error("Invalid payload.")],
|
||||
["message with empty array", [0x01, 0x90], new Error("Invalid payload.")],
|
||||
["message without outer array", [0x01, 0xc2], new Error("Invalid payload.")],
|
||||
["message with out-of-range message type", [0x03, 0x92, 0x05, 0x80], new Error("Invalid message type.")],
|
||||
["message with non-integer message type", [0x04, 0x92, 0xa1, 0x78, 0x80], new Error("Invalid message type.")],
|
||||
["message with invalid headers", [0x03, 0x92, 0x01, 0x05], new Error("Invalid headers.")],
|
||||
["Invocation message with invalid invocation id", [0x03, 0x92, 0x01, 0x80], new Error("Invalid payload for Invocation message.")],
|
||||
["StreamItem message with invalid invocation id", [0x03, 0x92, 0x02, 0x80], new Error("Invalid payload for stream Result message.")],
|
||||
["StreamItem message with invalid invocation id", [0x03, 0x92, 0x02, 0x80], new Error("Invalid payload for StreamItem message.")],
|
||||
["Completion message with invalid invocation id", [0x04, 0x93, 0x03, 0x80, 0xa0], new Error("Invalid payload for Completion message.")],
|
||||
["Completion message with unexpected result", [0x06, 0x95, 0x03, 0x80, 0xa0, 0x02, 0x00], new Error("Invalid payload for Completion message.")],
|
||||
["Completion message with missing result", [0x05, 0x94, 0x03, 0x80, 0xa0, 0x01], new Error("Invalid payload for Completion message.")],
|
||||
["Completion message with missing error", [0x05, 0x94, 0x03, 0x80, 0xa0, 0x03], new Error("Invalid payload for Completion message.")],
|
||||
] as Array<[string, number[], Error]>).forEach(([name, payload, expectedError]) =>
|
||||
it("throws for " + name, () => {
|
||||
expect(() => new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer))
|
||||
expect(() => new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer, new NullLogger()))
|
||||
.toThrow(expectedError);
|
||||
}));
|
||||
|
||||
|
|
@ -159,7 +165,7 @@ describe("MessageHubProtocol", () => {
|
|||
const payload = [
|
||||
0x08, 0x94, 0x02, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x08,
|
||||
0x0b, 0x95, 0x03, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b];
|
||||
const messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer);
|
||||
const messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer, new NullLogger());
|
||||
expect(messages).toEqual([
|
||||
{
|
||||
headers: {},
|
||||
|
|
@ -183,7 +189,7 @@ describe("MessageHubProtocol", () => {
|
|||
0x91, // message array length = 1 (fixarray)
|
||||
0x06, // type = 6 = Ping (fixnum)
|
||||
];
|
||||
const messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer);
|
||||
const messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer, new NullLogger());
|
||||
expect(messages).toEqual([
|
||||
{
|
||||
type: MessageType.Ping,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// 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.
|
||||
|
||||
import { CompletionMessage, HubMessage, IHubProtocol, InvocationMessage, MessageHeaders, MessageType, StreamInvocationMessage, StreamItemMessage, TransferFormat } from "@aspnet/signalr";
|
||||
import { CompletionMessage, HubMessage, IHubProtocol, ILogger, InvocationMessage, LogLevel, MessageHeaders, MessageType, NullLogger, StreamInvocationMessage, StreamItemMessage, TransferFormat } from "@aspnet/signalr";
|
||||
import { Buffer } from "buffer";
|
||||
import * as msgpack5 from "msgpack5";
|
||||
import { BinaryMessageFormat } from "./BinaryMessageFormat";
|
||||
|
|
@ -9,14 +9,18 @@ import { BinaryMessageFormat } from "./BinaryMessageFormat";
|
|||
export class MessagePackHubProtocol implements IHubProtocol {
|
||||
|
||||
public readonly name: string = "messagepack";
|
||||
public readonly version: number = 1;
|
||||
|
||||
public readonly transferFormat: TransferFormat = TransferFormat.Binary;
|
||||
|
||||
public parseMessages(input: ArrayBuffer): HubMessage[] {
|
||||
return BinaryMessageFormat.parse(input).map((m) => this.parseMessage(m));
|
||||
public parseMessages(input: ArrayBuffer, logger: ILogger): HubMessage[] {
|
||||
if (logger === null) {
|
||||
logger = new NullLogger();
|
||||
}
|
||||
return BinaryMessageFormat.parse(input).map((m) => this.parseMessage(m, logger));
|
||||
}
|
||||
|
||||
private parseMessage(input: Uint8Array): HubMessage {
|
||||
private parseMessage(input: Uint8Array, logger: ILogger): HubMessage {
|
||||
if (input.length === 0) {
|
||||
throw new Error("Invalid payload.");
|
||||
}
|
||||
|
|
@ -41,12 +45,15 @@ export class MessagePackHubProtocol implements IHubProtocol {
|
|||
case MessageType.Close:
|
||||
return this.createCloseMessage(properties);
|
||||
default:
|
||||
throw new Error("Invalid message type.");
|
||||
// Future protocol changes can add message types, old clients can ignore them
|
||||
logger.log(LogLevel.Information, "Unknown message type '" + messageType + "' ignored.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private createCloseMessage(properties: any[]): HubMessage {
|
||||
if (properties.length !== 2) {
|
||||
// check minimum length to allow protocol to add items to the end of objects in future releases
|
||||
if (properties.length < 2) {
|
||||
throw new Error("Invalid payload for Close message.");
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +65,8 @@ export class MessagePackHubProtocol implements IHubProtocol {
|
|||
}
|
||||
|
||||
private createPingMessage(properties: any[]): HubMessage {
|
||||
if (properties.length !== 1) {
|
||||
// check minimum length to allow protocol to add items to the end of objects in future releases
|
||||
if (properties.length < 1) {
|
||||
throw new Error("Invalid payload for Ping message.");
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +77,8 @@ export class MessagePackHubProtocol implements IHubProtocol {
|
|||
}
|
||||
|
||||
private createInvocationMessage(headers: MessageHeaders, properties: any[]): InvocationMessage {
|
||||
if (properties.length !== 5) {
|
||||
// check minimum length to allow protocol to add items to the end of objects in future releases
|
||||
if (properties.length < 5) {
|
||||
throw new Error("Invalid payload for Invocation message.");
|
||||
}
|
||||
|
||||
|
|
@ -94,8 +103,9 @@ export class MessagePackHubProtocol implements IHubProtocol {
|
|||
}
|
||||
|
||||
private createStreamItemMessage(headers: MessageHeaders, properties: any[]): StreamItemMessage {
|
||||
if (properties.length !== 4) {
|
||||
throw new Error("Invalid payload for stream Result message.");
|
||||
// check minimum length to allow protocol to add items to the end of objects in future releases
|
||||
if (properties.length < 4) {
|
||||
throw new Error("Invalid payload for StreamItem message.");
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -107,6 +117,7 @@ export class MessagePackHubProtocol implements IHubProtocol {
|
|||
}
|
||||
|
||||
private createCompletionMessage(headers: MessageHeaders, properties: any[]): CompletionMessage {
|
||||
// check minimum length to allow protocol to add items to the end of objects in future releases
|
||||
if (properties.length < 4) {
|
||||
throw new Error("Invalid payload for Completion message.");
|
||||
}
|
||||
|
|
@ -117,8 +128,7 @@ export class MessagePackHubProtocol implements IHubProtocol {
|
|||
|
||||
const resultKind = properties[3];
|
||||
|
||||
if ((resultKind === voidResult && properties.length !== 4) ||
|
||||
(resultKind !== voidResult && properties.length !== 5)) {
|
||||
if (resultKind !== voidResult && properties.length < 5) {
|
||||
throw new Error("Invalid payload for Completion message.");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { ConnectionClosed, DataReceived } from "../src/Common";
|
||||
import { HubConnection } from "../src/HubConnection";
|
||||
import { IConnection } from "../src/IConnection";
|
||||
import { MessageType, IHubProtocol, HubMessage } from "../src/IHubProtocol";
|
||||
import { HubMessage, IHubProtocol, MessageType } from "../src/IHubProtocol";
|
||||
import { ILogger, LogLevel } from "../src/ILogger";
|
||||
import { Observer } from "../src/Observable";
|
||||
import { TextMessageFormat } from "../src/TextMessageFormat";
|
||||
|
|
@ -27,6 +27,7 @@ describe("HubConnection", () => {
|
|||
expect(connection.sentData.length).toBe(1);
|
||||
expect(JSON.parse(connection.sentData[0])).toEqual({
|
||||
protocol: "json",
|
||||
version: 1,
|
||||
});
|
||||
await hubConnection.stop();
|
||||
});
|
||||
|
|
@ -203,7 +204,7 @@ describe("HubConnection", () => {
|
|||
const connection = new TestConnection();
|
||||
|
||||
const hubConnection = new HubConnection(connection, commonOptions);
|
||||
|
||||
|
||||
connection.receiveHandshakeResponse();
|
||||
|
||||
const invokePromise = hubConnection.invoke("testMethod");
|
||||
|
|
@ -232,7 +233,7 @@ describe("HubConnection", () => {
|
|||
|
||||
connection.receive({
|
||||
arguments: ["test"],
|
||||
invocationId: 0,
|
||||
invocationId: "0",
|
||||
nonblocking: true,
|
||||
target: "message",
|
||||
type: MessageType.Invocation,
|
||||
|
|
@ -240,7 +241,7 @@ describe("HubConnection", () => {
|
|||
|
||||
expect(warnings).toEqual(["No client method with the name 'message' found."]);
|
||||
});
|
||||
|
||||
|
||||
it("invocations ignored in callbacks that have registered then unregistered", async () => {
|
||||
const warnings: string[] = [];
|
||||
const logger = {
|
||||
|
|
@ -261,7 +262,7 @@ describe("HubConnection", () => {
|
|||
|
||||
connection.receive({
|
||||
arguments: ["test"],
|
||||
invocationId: 0,
|
||||
invocationId: "0",
|
||||
nonblocking: true,
|
||||
target: "message",
|
||||
type: MessageType.Invocation,
|
||||
|
|
@ -273,7 +274,7 @@ describe("HubConnection", () => {
|
|||
it("callback invoked when servers invokes a method on the client", async () => {
|
||||
const connection = new TestConnection();
|
||||
const hubConnection = new HubConnection(connection, commonOptions);
|
||||
|
||||
|
||||
connection.receiveHandshakeResponse();
|
||||
|
||||
let value = "";
|
||||
|
|
@ -281,7 +282,7 @@ describe("HubConnection", () => {
|
|||
|
||||
connection.receive({
|
||||
arguments: ["test"],
|
||||
invocationId: 0,
|
||||
invocationId: "0",
|
||||
nonblocking: true,
|
||||
target: "message",
|
||||
type: MessageType.Invocation,
|
||||
|
|
@ -293,7 +294,7 @@ describe("HubConnection", () => {
|
|||
it("stop on handshake error", async () => {
|
||||
const connection = new TestConnection();
|
||||
const hubConnection = new HubConnection(connection, commonOptions);
|
||||
|
||||
|
||||
let closeError: Error = null;
|
||||
hubConnection.onclose((e) => closeError = e);
|
||||
|
||||
|
|
@ -305,7 +306,7 @@ describe("HubConnection", () => {
|
|||
it("stop on close message", async () => {
|
||||
const connection = new TestConnection();
|
||||
const hubConnection = new HubConnection(connection, commonOptions);
|
||||
|
||||
|
||||
let isClosed = false;
|
||||
let closeError: Error = null;
|
||||
hubConnection.onclose((e) => {
|
||||
|
|
@ -326,7 +327,7 @@ describe("HubConnection", () => {
|
|||
it("stop on error close message", async () => {
|
||||
const connection = new TestConnection();
|
||||
const hubConnection = new HubConnection(connection, commonOptions);
|
||||
|
||||
|
||||
let isClosed = false;
|
||||
let closeError: Error = null;
|
||||
hubConnection.onclose((e) => {
|
||||
|
|
@ -348,7 +349,7 @@ describe("HubConnection", () => {
|
|||
it("can have multiple callbacks", async () => {
|
||||
const connection = new TestConnection();
|
||||
const hubConnection = new HubConnection(connection, commonOptions);
|
||||
|
||||
|
||||
connection.receiveHandshakeResponse();
|
||||
|
||||
let numInvocations1 = 0;
|
||||
|
|
@ -358,7 +359,7 @@ describe("HubConnection", () => {
|
|||
|
||||
connection.receive({
|
||||
arguments: [],
|
||||
invocationId: 0,
|
||||
invocationId: "0",
|
||||
nonblocking: true,
|
||||
target: "message",
|
||||
type: MessageType.Invocation,
|
||||
|
|
@ -380,7 +381,7 @@ describe("HubConnection", () => {
|
|||
|
||||
connection.receive({
|
||||
arguments: [],
|
||||
invocationId: 0,
|
||||
invocationId: "0",
|
||||
nonblocking: true,
|
||||
target: "message",
|
||||
type: MessageType.Invocation,
|
||||
|
|
@ -390,7 +391,7 @@ describe("HubConnection", () => {
|
|||
|
||||
connection.receive({
|
||||
arguments: [],
|
||||
invocationId: 0,
|
||||
invocationId: "0",
|
||||
nonblocking: true,
|
||||
target: "message",
|
||||
type: MessageType.Invocation,
|
||||
|
|
@ -434,7 +435,7 @@ describe("HubConnection", () => {
|
|||
// invoke a method to make sure we are not trying to use null/undefined
|
||||
connection.receive({
|
||||
arguments: [],
|
||||
invocationId: 0,
|
||||
invocationId: "0",
|
||||
nonblocking: true,
|
||||
target: "message",
|
||||
type: MessageType.Invocation,
|
||||
|
|
@ -493,7 +494,7 @@ describe("HubConnection", () => {
|
|||
it("completes the observer when a completion is received", async () => {
|
||||
const connection = new TestConnection();
|
||||
const hubConnection = new HubConnection(connection, commonOptions);
|
||||
|
||||
|
||||
connection.receiveHandshakeResponse();
|
||||
|
||||
const observer = new TestObserver();
|
||||
|
|
@ -779,6 +780,7 @@ class TestConnection implements IConnection {
|
|||
|
||||
class TestProtocol implements IHubProtocol {
|
||||
public readonly name: string = "TestProtocol";
|
||||
public readonly version: number = 1;
|
||||
|
||||
public readonly transferFormat: TransferFormat;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
// 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.
|
||||
|
||||
import { CompletionMessage, InvocationMessage, MessageType, StreamItemMessage } from "../src/IHubProtocol";
|
||||
import { JsonHubProtocol } from "../src/JsonHubProtocol";
|
||||
import { NullLogger } from "../src/Loggers";
|
||||
import { TextMessageFormat } from "../src/TextMessageFormat";
|
||||
|
||||
describe("JsonHubProtocol", () => {
|
||||
it("can write/read non-blocking Invocation message", () => {
|
||||
const invocation = {
|
||||
arguments: [42, true, "test", ["x1", "y2"], null],
|
||||
headers: {},
|
||||
target: "myMethod",
|
||||
type: MessageType.Invocation,
|
||||
} as InvocationMessage;
|
||||
|
||||
const protocol = new JsonHubProtocol();
|
||||
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), new NullLogger());
|
||||
expect(parsedMessages).toEqual([invocation]);
|
||||
});
|
||||
|
||||
it("can read Invocation message with Date argument", () => {
|
||||
const invocation = {
|
||||
arguments: [Date.UTC(2018, 1, 1, 12, 34, 56)],
|
||||
headers: {},
|
||||
target: "mymethod",
|
||||
type: MessageType.Invocation,
|
||||
} as InvocationMessage;
|
||||
|
||||
const protocol = new JsonHubProtocol();
|
||||
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), new NullLogger());
|
||||
expect(parsedMessages).toEqual([invocation]);
|
||||
});
|
||||
|
||||
it("can write/read Invocation message with headers", () => {
|
||||
const invocation = {
|
||||
arguments: [42, true, "test", ["x1", "y2"], null],
|
||||
headers: {
|
||||
foo: "bar",
|
||||
},
|
||||
target: "myMethod",
|
||||
type: MessageType.Invocation,
|
||||
} as InvocationMessage;
|
||||
|
||||
const protocol = new JsonHubProtocol();
|
||||
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), new NullLogger());
|
||||
expect(parsedMessages).toEqual([invocation]);
|
||||
});
|
||||
|
||||
it("can write/read Invocation message", () => {
|
||||
const invocation = {
|
||||
arguments: [42, true, "test", ["x1", "y2"], null],
|
||||
headers: {},
|
||||
invocationId: "123",
|
||||
target: "myMethod",
|
||||
type: MessageType.Invocation,
|
||||
} as InvocationMessage;
|
||||
|
||||
const protocol = new JsonHubProtocol();
|
||||
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), new NullLogger());
|
||||
expect(parsedMessages).toEqual([invocation]);
|
||||
});
|
||||
|
||||
([
|
||||
[`{"type":3, "invocationId": "abc", "error": "Err", "result": null, "headers": {}}${TextMessageFormat.RecordSeparator}`,
|
||||
{
|
||||
error: "Err",
|
||||
headers: {},
|
||||
invocationId: "abc",
|
||||
result: null,
|
||||
type: MessageType.Completion,
|
||||
} as CompletionMessage],
|
||||
[`{"type":3, "invocationId": "abc", "result": "OK", "error": null, "headers": {}}${TextMessageFormat.RecordSeparator}`,
|
||||
{
|
||||
error: null,
|
||||
headers: {},
|
||||
invocationId: "abc",
|
||||
result: "OK",
|
||||
type: MessageType.Completion,
|
||||
} as CompletionMessage],
|
||||
[`{"type":3, "invocationId": "abc", "error": null, "result": null, "headers": {}}${TextMessageFormat.RecordSeparator}`,
|
||||
{
|
||||
error: null,
|
||||
headers: {},
|
||||
invocationId: "abc",
|
||||
result: null,
|
||||
type: MessageType.Completion,
|
||||
} as CompletionMessage],
|
||||
[`{"type":3, "invocationId": "abc", "result": 1514805840000, "error": null, "headers": {}}${TextMessageFormat.RecordSeparator}`,
|
||||
{
|
||||
error: null,
|
||||
headers: {},
|
||||
invocationId: "abc",
|
||||
result: Date.UTC(2018, 0, 1, 11, 24, 0),
|
||||
type: MessageType.Completion,
|
||||
} as CompletionMessage],
|
||||
[`{"type":3, "invocationId": "abc", "error": null, "result": null, "headers": {}, "extraParameter":"value"}${TextMessageFormat.RecordSeparator}`,
|
||||
{
|
||||
error: null,
|
||||
extraParameter: "value",
|
||||
headers: {},
|
||||
invocationId: "abc",
|
||||
result: null,
|
||||
type: MessageType.Completion,
|
||||
} as CompletionMessage],
|
||||
] as Array<[string, CompletionMessage]>).forEach(([payload, expectedMessage]) =>
|
||||
it("can read Completion message", () => {
|
||||
const messages = new JsonHubProtocol().parseMessages(payload, new NullLogger());
|
||||
expect(messages).toEqual([expectedMessage]);
|
||||
}));
|
||||
|
||||
([
|
||||
[`{"type":2, "invocationId": "abc", "headers": {}, "item": 8}${TextMessageFormat.RecordSeparator}`,
|
||||
{
|
||||
headers: {},
|
||||
invocationId: "abc",
|
||||
item: 8,
|
||||
type: MessageType.StreamItem,
|
||||
} as StreamItemMessage],
|
||||
[`{"type":2, "invocationId": "abc", "headers": {}, "item": 1514805840000}${TextMessageFormat.RecordSeparator}`,
|
||||
{
|
||||
headers: {},
|
||||
invocationId: "abc",
|
||||
item: Date.UTC(2018, 0, 1, 11, 24, 0),
|
||||
type: MessageType.StreamItem,
|
||||
} as StreamItemMessage],
|
||||
] as Array<[string, StreamItemMessage]>).forEach(([payload, expectedMessage]) =>
|
||||
it("can read StreamItem message", () => {
|
||||
const messages = new JsonHubProtocol().parseMessages(payload, new NullLogger());
|
||||
expect(messages).toEqual([expectedMessage]);
|
||||
}));
|
||||
|
||||
([
|
||||
[`{"type":2, "invocationId": "abc", "headers": {"t": "u"}, "item": 8}${TextMessageFormat.RecordSeparator}`,
|
||||
{
|
||||
headers: {
|
||||
t: "u",
|
||||
},
|
||||
invocationId: "abc",
|
||||
item: 8,
|
||||
type: MessageType.StreamItem,
|
||||
} as StreamItemMessage],
|
||||
] as Array<[string, StreamItemMessage]>).forEach(([payload, expectedMessage]) =>
|
||||
it("can read message with headers", () => {
|
||||
const messages = new JsonHubProtocol().parseMessages(payload, new NullLogger());
|
||||
expect(messages).toEqual([expectedMessage]);
|
||||
}));
|
||||
|
||||
([
|
||||
["message with empty payload", `{}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload.")],
|
||||
["Invocation message with invalid invocation id", `{"type":1,"invocationId":1,"target":"method"}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for Invocation message.")],
|
||||
["Invocation message with empty string invocation id", `{"type":1,"invocationId":"","target":"method"}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for Invocation message.")],
|
||||
["Invocation message with invalid target", `{"type":1,"invocationId":"1","target":1}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for Invocation message.")],
|
||||
["StreamItem message with missing invocation id", `{"type":2}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for StreamItem message.")],
|
||||
["StreamItem message with invalid invocation id", `{"type":2,"invocationId":1}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for StreamItem message.")],
|
||||
["Completion message with missing invocation id", `{"type":3}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for Completion message.")],
|
||||
["Completion message with invalid invocation id", `{"type":3,"invocationId":1}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for Completion message.")],
|
||||
["Completion message with result and error", `{"type":3,"invocationId":"1","result":2,"error":"error"}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for Completion message.")],
|
||||
["Completion message with non-string error", `{"type":3,"invocationId":"1","error":21}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for Completion message.")],
|
||||
] as Array<[string, string, Error]>).forEach(([name, payload, expectedError]) =>
|
||||
it("throws for " + name, () => {
|
||||
expect(() => new JsonHubProtocol().parseMessages(payload, new NullLogger()))
|
||||
.toThrow(expectedError);
|
||||
}));
|
||||
|
||||
it("can read multiple messages", () => {
|
||||
const payload = `{"type":2, "invocationId": "abc", "headers": {}, "item": 8}${TextMessageFormat.RecordSeparator}{"type":3, "invocationId": "abc", "headers": {}, "result": "OK", "error": null}${TextMessageFormat.RecordSeparator}`;
|
||||
const messages = new JsonHubProtocol().parseMessages(payload, new NullLogger());
|
||||
expect(messages).toEqual([
|
||||
{
|
||||
headers: {},
|
||||
invocationId: "abc",
|
||||
item: 8,
|
||||
type: MessageType.StreamItem,
|
||||
} as StreamItemMessage,
|
||||
{
|
||||
error: null,
|
||||
headers: {},
|
||||
invocationId: "abc",
|
||||
result: "OK",
|
||||
type: MessageType.Completion,
|
||||
} as CompletionMessage,
|
||||
]);
|
||||
});
|
||||
|
||||
it("can read ping message", () => {
|
||||
const payload = `{"type":6}${TextMessageFormat.RecordSeparator}`;
|
||||
const messages = new JsonHubProtocol().parseMessages(payload, new NullLogger());
|
||||
expect(messages).toEqual([
|
||||
{
|
||||
type: MessageType.Ping,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -70,7 +70,7 @@ export class HubConnection {
|
|||
// Data may have all been read when processing handshake response
|
||||
if (data) {
|
||||
// Parse the messages
|
||||
const messages = this.protocol.parseMessages(data);
|
||||
const messages = this.protocol.parseMessages(data, this.logger);
|
||||
|
||||
for (const message of messages) {
|
||||
switch (message.type) {
|
||||
|
|
@ -211,7 +211,7 @@ export class HubConnection {
|
|||
// Handshake request is always JSON
|
||||
await this.connection.send(
|
||||
TextMessageFormat.write(
|
||||
JSON.stringify({ protocol: this.protocol.name } as HandshakeRequestMessage)));
|
||||
JSON.stringify({ protocol: this.protocol.name, version: this.protocol.version } as HandshakeRequestMessage)));
|
||||
|
||||
this.logger.log(LogLevel.Information, `Using HubProtocol '${this.protocol.name}'.`);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { TransferFormat } from "./Transports";
|
||||
import { ILogger } from "./ILogger";
|
||||
import { TransferFormat } from "./Transports";
|
||||
|
||||
// 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.
|
||||
|
|
@ -51,6 +52,7 @@ export interface CompletionMessage extends HubInvocationMessage {
|
|||
|
||||
export interface HandshakeRequestMessage {
|
||||
readonly protocol: string;
|
||||
readonly version: number;
|
||||
}
|
||||
|
||||
export interface HandshakeResponseMessage {
|
||||
|
|
@ -72,7 +74,8 @@ export interface CancelInvocationMessage extends HubInvocationMessage {
|
|||
|
||||
export interface IHubProtocol {
|
||||
readonly name: string;
|
||||
readonly version: number;
|
||||
readonly transferFormat: TransferFormat;
|
||||
parseMessages(input: any): HubMessage[];
|
||||
parseMessages(input: any, logger: ILogger): HubMessage[];
|
||||
writeMessage(message: HubMessage): any;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
// 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.
|
||||
|
||||
import { HubMessage, IHubProtocol } from "./IHubProtocol";
|
||||
import { CloseMessage, CompletionMessage, HubMessage, IHubProtocol, InvocationMessage, MessageType, PingMessage, StreamItemMessage } from "./IHubProtocol";
|
||||
import { ILogger, LogLevel } from "./ILogger";
|
||||
import { NullLogger } from "./Loggers";
|
||||
import { TextMessageFormat } from "./TextMessageFormat";
|
||||
import { TransferFormat } from "./Transports";
|
||||
|
||||
|
|
@ -10,19 +12,50 @@ export const JSON_HUB_PROTOCOL_NAME: string = "json";
|
|||
export class JsonHubProtocol implements IHubProtocol {
|
||||
|
||||
public readonly name: string = JSON_HUB_PROTOCOL_NAME;
|
||||
public readonly version: number = 1;
|
||||
|
||||
public readonly transferFormat: TransferFormat = TransferFormat.Text;
|
||||
|
||||
public parseMessages(input: string): HubMessage[] {
|
||||
public parseMessages(input: string, logger: ILogger): HubMessage[] {
|
||||
if (!input) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (logger === null) {
|
||||
logger = new NullLogger();
|
||||
}
|
||||
|
||||
// Parse the messages
|
||||
const messages = TextMessageFormat.parse(input);
|
||||
|
||||
const hubMessages = [];
|
||||
for (const message of messages) {
|
||||
hubMessages.push(JSON.parse(message));
|
||||
const parsedMessage = JSON.parse(message) as HubMessage;
|
||||
if (typeof parsedMessage.type !== "number") {
|
||||
throw new Error("Invalid payload.");
|
||||
}
|
||||
switch (parsedMessage.type) {
|
||||
case MessageType.Invocation:
|
||||
this.isInvocationMessage(parsedMessage);
|
||||
break;
|
||||
case MessageType.StreamItem:
|
||||
this.isStreamItemMessage(parsedMessage);
|
||||
break;
|
||||
case MessageType.Completion:
|
||||
this.isCompletionMessage(parsedMessage);
|
||||
break;
|
||||
case MessageType.Ping:
|
||||
// Single value, no need to validate
|
||||
break;
|
||||
case MessageType.Close:
|
||||
// All optional values, no need to validate
|
||||
break;
|
||||
default:
|
||||
// Future protocol changes can add message types, old clients can ignore them
|
||||
logger.log(LogLevel.Information, "Unknown message type '" + parsedMessage.type + "' ignored.");
|
||||
continue;
|
||||
}
|
||||
hubMessages.push(parsedMessage);
|
||||
}
|
||||
|
||||
return hubMessages;
|
||||
|
|
@ -31,4 +64,38 @@ export class JsonHubProtocol implements IHubProtocol {
|
|||
public writeMessage(message: HubMessage): string {
|
||||
return TextMessageFormat.write(JSON.stringify(message));
|
||||
}
|
||||
|
||||
private isInvocationMessage(message: InvocationMessage): void {
|
||||
this.assertNotEmptyString(message.target, "Invalid payload for Invocation message.");
|
||||
|
||||
if (message.invocationId !== undefined) {
|
||||
this.assertNotEmptyString(message.invocationId, "Invalid payload for Invocation message.");
|
||||
}
|
||||
}
|
||||
|
||||
private isStreamItemMessage(message: StreamItemMessage): void {
|
||||
this.assertNotEmptyString(message.invocationId, "Invalid payload for StreamItem message.");
|
||||
|
||||
if (message.item === undefined) {
|
||||
throw new Error("Invalid payload for StreamItem message.");
|
||||
}
|
||||
}
|
||||
|
||||
private isCompletionMessage(message: CompletionMessage): void {
|
||||
if (message.result && message.error) {
|
||||
throw new Error("Invalid payload for Completion message.");
|
||||
}
|
||||
|
||||
if (!message.result && message.error) {
|
||||
this.assertNotEmptyString(message.error, "Invalid payload for Completion message.");
|
||||
}
|
||||
|
||||
this.assertNotEmptyString(message.invocationId, "Invalid payload for Completion message.");
|
||||
}
|
||||
|
||||
private assertNotEmptyString(value: any, errorMessage: string): void {
|
||||
if (typeof value !== "string" || value === "") {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,23 +32,25 @@ In the SignalR protocol, the following types of messages can be sent:
|
|||
| `CancelInvocation` | Caller | Sent by the client to cancel a streaming invocation on the server. |
|
||||
| `Ping` | Caller, Callee | Sent by either party to check if the connection is active. |
|
||||
|
||||
After opening a connection to the server the client must send a `HandshakeRequest` message to the server as its first message. The handshake message is **always** a JSON message and contains the name of the format (protocol) that will be used for the duration of the connection. The server will reply with a `HandshakeResponse`, also always JSON, containing an error if the server does not support the protocol. If the server does not support the protocol requested by the client or the first message received from the client is not a `HandshakeRequest` message the server must close the connection.
|
||||
After opening a connection to the server the client must send a `HandshakeRequest` message to the server as its first message. The handshake message is **always** a JSON message and contains the name of the format (protocol) as well as the version of the protocol that will be used for the duration of the connection. The server will reply with a `HandshakeResponse`, also always JSON, containing an error if the server does not support the protocol. If the server does not support the protocol requested by the client or the first message received from the client is not a `HandshakeRequest` message the server must close the connection.
|
||||
|
||||
The `HandshakeRequest` message contains the following properties:
|
||||
|
||||
* `protocol` - the name of the protocol to be used for messages exchanged between the server and the client
|
||||
* `version` - the value must always be 1, for both MessagePack and Json protocols
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"protocol": "messagepack"
|
||||
"protocol": "messagepack",
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
|
||||
The `HandshakeResponse` message contains the following properties:
|
||||
|
||||
* `error` - the optional error message if the server does not support the request protocol
|
||||
* `error` - the optional error message if the server does not support the requested protocol
|
||||
|
||||
Example:
|
||||
|
||||
|
|
|
|||
|
|
@ -80,8 +80,8 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
private static readonly Action<ILogger, string, Exception> _receivedUnexpectedResponse =
|
||||
LoggerMessage.Define<string>(LogLevel.Error, new EventId(23, "ReceivedUnexpectedResponse"), "Unsolicited response received for invocation '{InvocationId}'.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _hubProtocol =
|
||||
LoggerMessage.Define<string>(LogLevel.Information, new EventId(24, "HubProtocol"), "Using HubProtocol '{Protocol}'.");
|
||||
private static readonly Action<ILogger, string, int, Exception> _hubProtocol =
|
||||
LoggerMessage.Define<string, int>(LogLevel.Information, new EventId(24, "HubProtocol"), "Using HubProtocol '{Protocol} v{Version}'.");
|
||||
|
||||
private static readonly Action<ILogger, string, string, string, int, Exception> _preparingStreamingInvocation =
|
||||
LoggerMessage.Define<string, string, string, int>(LogLevel.Trace, new EventId(25, "PreparingStreamingInvocation"), "Preparing streaming invocation '{InvocationId}' of '{Target}', with return type '{ReturnType}' and {ArgumentCount} argument(s).");
|
||||
|
|
@ -253,9 +253,9 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
_receivedUnexpectedResponse(logger, invocationId, null);
|
||||
}
|
||||
|
||||
public static void HubProtocol(ILogger logger, string hubProtocol)
|
||||
public static void HubProtocol(ILogger logger, string hubProtocol, int version)
|
||||
{
|
||||
_hubProtocol(logger, hubProtocol, null);
|
||||
_hubProtocol(logger, hubProtocol, version, null);
|
||||
}
|
||||
|
||||
public static void ResettingKeepAliveTimer(ILogger logger)
|
||||
|
|
|
|||
|
|
@ -115,13 +115,13 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
_needKeepAlive = _connection.Features.Get<IConnectionInherentKeepAliveFeature>() == null;
|
||||
_receivedHandshakeResponse = false;
|
||||
|
||||
Log.HubProtocol(_logger, _protocol.Name);
|
||||
Log.HubProtocol(_logger, _protocol.Name, _protocol.Version);
|
||||
|
||||
_connectionActive = new CancellationTokenSource();
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
Log.SendingHubHandshake(_logger);
|
||||
HandshakeProtocol.WriteRequestMessage(new HandshakeRequestMessage(_protocol.Name), memoryStream);
|
||||
HandshakeProtocol.WriteRequestMessage(new HandshakeRequestMessage(_protocol.Name, _protocol.Version), memoryStream);
|
||||
await _connection.SendAsync(memoryStream.ToArray(), _connectionActive.Token);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Formatters;
|
||||
|
|
@ -17,6 +16,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
private static readonly UTF8Encoding _utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
||||
|
||||
private const string ProtocolPropertyName = "protocol";
|
||||
private const string ProtocolVersionName = "version";
|
||||
private const string ErrorPropertyName = "error";
|
||||
private const string TypePropertyName = "type";
|
||||
|
||||
|
|
@ -27,6 +27,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
writer.WriteStartObject();
|
||||
writer.WritePropertyName(ProtocolPropertyName);
|
||||
writer.WriteValue(requestMessage.Protocol);
|
||||
writer.WritePropertyName(ProtocolVersionName);
|
||||
writer.WriteValue(requestMessage.Version);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +103,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
var token = JToken.ReadFrom(reader);
|
||||
var handshakeJObject = JsonUtils.GetObject(token);
|
||||
var protocol = JsonUtils.GetRequiredProperty<string>(handshakeJObject, ProtocolPropertyName);
|
||||
requestMessage = new HandshakeRequestMessage(protocol);
|
||||
var protocolVersion = JsonUtils.GetRequiredProperty<int>(handshakeJObject, ProtocolVersionName, JTokenType.Integer);
|
||||
requestMessage = new HandshakeRequestMessage(protocol, protocolVersion);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
{
|
||||
public class HandshakeRequestMessage : HubMessage
|
||||
{
|
||||
public HandshakeRequestMessage(string protocol)
|
||||
public HandshakeRequestMessage(string protocol, int version)
|
||||
{
|
||||
Protocol = protocol;
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public string Protocol { get; }
|
||||
public int Version { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,14 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
{
|
||||
string Name { get; }
|
||||
|
||||
int Version { get; }
|
||||
|
||||
TransferFormat TransferFormat { get; }
|
||||
|
||||
bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages);
|
||||
|
||||
void WriteMessage(HubMessage message, Stream output);
|
||||
|
||||
bool IsVersionSupported(int version);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
private const string HeadersPropertyName = "headers";
|
||||
|
||||
public static readonly string ProtocolName = "json";
|
||||
public static readonly int ProtocolVersion = 1;
|
||||
|
||||
// ONLY to be used for application payloads (args, return values, etc.)
|
||||
public JsonSerializer PayloadSerializer { get; }
|
||||
|
|
@ -44,14 +45,25 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
|
||||
public string Name => ProtocolName;
|
||||
|
||||
public int Version => ProtocolVersion;
|
||||
|
||||
public TransferFormat TransferFormat => TransferFormat.Text;
|
||||
|
||||
public bool IsVersionSupported(int version)
|
||||
{
|
||||
return version == Version;
|
||||
}
|
||||
|
||||
public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
||||
{
|
||||
while (TextMessageParser.TryParseMessage(ref input, out var payload))
|
||||
{
|
||||
var textReader = new Utf8BufferTextReader(payload);
|
||||
messages.Add(ParseMessage(textReader, binder));
|
||||
var message = ParseMessage(textReader, binder);
|
||||
if (message != null)
|
||||
{
|
||||
messages.Add(message);
|
||||
}
|
||||
}
|
||||
|
||||
return messages.Count > 0;
|
||||
|
|
@ -277,7 +289,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
case null:
|
||||
throw new InvalidDataException($"Missing required property '{TypePropertyName}'.");
|
||||
default:
|
||||
throw new InvalidDataException($"Unknown message type: {type}");
|
||||
// Future protocol changes can add message types, old clients can ignore them
|
||||
return null;
|
||||
}
|
||||
|
||||
return ApplyHeaders(message, headers);
|
||||
|
|
|
|||
|
|
@ -239,6 +239,14 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!Protocol.IsVersionSupported(handshakeRequestMessage.Version))
|
||||
{
|
||||
Log.ProtocolVersionFailed(_logger, handshakeRequestMessage.Protocol, handshakeRequestMessage.Version);
|
||||
await WriteHandshakeResponseAsync(new HandshakeResponseMessage(
|
||||
$"The server does not support version {handshakeRequestMessage.Version} of the '{handshakeRequestMessage.Protocol}' protocol."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there's a transfer format feature, we need to check if we're compatible and set the active format.
|
||||
// If there isn't a feature, it means that the transport supports binary data and doesn't need us to tell them
|
||||
// what format we're writing.
|
||||
|
|
@ -371,6 +379,9 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
private static readonly Action<ILogger, Exception> _failedWritingMessage =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(6, "FailedWritingMessage"), "Failed writing message.");
|
||||
|
||||
private static readonly Action<ILogger, string, int, Exception> _protocolVersionFailed =
|
||||
LoggerMessage.Define<string, int>(LogLevel.Warning, new EventId(7, "ProtocolVersionFailed"), "Server does not support version {Version} of the {Protocol} protocol.");
|
||||
|
||||
public static void HandshakeComplete(ILogger logger, string hubProtocol)
|
||||
{
|
||||
_handshakeComplete(logger, hubProtocol, null);
|
||||
|
|
@ -400,6 +411,11 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
{
|
||||
_failedWritingMessage(logger, exception);
|
||||
}
|
||||
|
||||
public static void ProtocolVersionFailed(ILogger logger, string protocolName, int version)
|
||||
{
|
||||
_protocolVersionFailed(logger, protocolName, version, null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ using System.Runtime.ExceptionServices;
|
|||
using System.Runtime.InteropServices;
|
||||
using Microsoft.AspNetCore.Protocols;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Formatters;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MsgPack;
|
||||
using MsgPack.Serialization;
|
||||
|
|
@ -23,11 +22,14 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
private const int NonVoidResult = 3;
|
||||
|
||||
public static readonly string ProtocolName = "messagepack";
|
||||
public static readonly int ProtocolVersion = 1;
|
||||
|
||||
public SerializationContext SerializationContext { get; }
|
||||
|
||||
public string Name => ProtocolName;
|
||||
|
||||
public int Version => ProtocolVersion;
|
||||
|
||||
public TransferFormat TransferFormat => TransferFormat.Binary;
|
||||
|
||||
public MessagePackHubProtocol()
|
||||
|
|
@ -39,6 +41,11 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
SerializationContext = options.Value.SerializationContext;
|
||||
}
|
||||
|
||||
public bool IsVersionSupported(int version)
|
||||
{
|
||||
return version == Version;
|
||||
}
|
||||
|
||||
public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
||||
{
|
||||
while (BinaryMessageParser.TryParseMessage(ref input, out var payload))
|
||||
|
|
@ -46,7 +53,11 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
var isArray = MemoryMarshal.TryGetArray(payload, out var arraySegment);
|
||||
// This will never be false unless we started using un-managed buffers
|
||||
Debug.Assert(isArray);
|
||||
messages.Add(ParseMessage(arraySegment.Array, arraySegment.Offset, binder));
|
||||
var message = ParseMessage(arraySegment.Array, arraySegment.Offset, binder);
|
||||
if (message != null)
|
||||
{
|
||||
messages.Add(message);
|
||||
}
|
||||
}
|
||||
|
||||
return messages.Count > 0;
|
||||
|
|
@ -77,7 +88,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
|
|||
case HubProtocolConstants.CloseMessageType:
|
||||
return CreateCloseMessage(unpacker);
|
||||
default:
|
||||
throw new FormatException($"Invalid message type: {messageType}.");
|
||||
// Future protocol changes can add message types, old clients can ignore them
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,10 @@
|
|||
// 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.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
|
|
@ -55,7 +50,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
var handshakeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
|
||||
|
||||
Assert.Equal("{\"protocol\":\"json\"}\u001e", handshakeMessage);
|
||||
Assert.Equal("{\"protocol\":\"json\",\"version\":1}\u001e", handshakeMessage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Http.Features;
|
|||
using Microsoft.AspNetCore.Protocols;
|
||||
using Microsoft.AspNetCore.SignalR.Internal;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
|
@ -252,9 +251,15 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
}
|
||||
|
||||
public string Name => "MockHubProtocol";
|
||||
public int Version => 1;
|
||||
|
||||
public TransferFormat TransferFormat => TransferFormat.Binary;
|
||||
|
||||
public bool IsVersionSupported(int version)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
||||
{
|
||||
ParseCalls += 1;
|
||||
|
|
|
|||
|
|
@ -13,16 +13,17 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
public class HandshakeProtocolTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("{\"protocol\":\"dummy\"}\u001e", "dummy")]
|
||||
[InlineData("{\"protocol\":\"\"}\u001e", "")]
|
||||
[InlineData("{\"protocol\":null}\u001e", null)]
|
||||
public void ParsingHandshakeRequestMessageSuccessForValidMessages(string json, string protocol)
|
||||
[InlineData("{\"protocol\":\"dummy\",\"version\":1}\u001e", "dummy", 1)]
|
||||
[InlineData("{\"protocol\":\"\",\"version\":10}\u001e", "", 10)]
|
||||
[InlineData("{\"protocol\":null,\"version\":123}\u001e", null, 123)]
|
||||
public void ParsingHandshakeRequestMessageSuccessForValidMessages(string json, string protocol, int version)
|
||||
{
|
||||
var message = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
Assert.True(HandshakeProtocol.TryParseRequestMessage(new ReadOnlySequence<byte>(message), out var deserializedMessage, out _, out _));
|
||||
|
||||
Assert.Equal(protocol, deserializedMessage.Protocol);
|
||||
Assert.Equal(version, deserializedMessage.Version);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -53,6 +54,9 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
[InlineData("null\u001e", "Unexpected JSON Token Type 'Null'. Expected a JSON Object.")]
|
||||
[InlineData("{}\u001e", "Missing required property 'protocol'.")]
|
||||
[InlineData("[]\u001e", "Unexpected JSON Token Type 'Array'. Expected a JSON Object.")]
|
||||
[InlineData("{\"protocol\":\"json\"}\u001e", "Missing required property 'version'.")]
|
||||
[InlineData("{\"version\":1}\u001e", "Missing required property 'protocol'.")]
|
||||
[InlineData("{\"protocol\":null,\"version\":\"123\"}\u001e", "Expected 'version' to be of type Integer.")]
|
||||
public void ParsingHandshakeRequestMessageThrowsForInvalidMessages(string payload, string expectedMessage)
|
||||
{
|
||||
var message = Encoding.UTF8.GetBytes(payload);
|
||||
|
|
|
|||
|
|
@ -173,7 +173,6 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
[InlineData("{'type':4,'invocationId':'42','target':'foo'}", "Missing required property 'arguments'.")]
|
||||
[InlineData("{'type':4,'invocationId':'42','target':'foo','arguments':{}}", "Expected 'arguments' to be of type Array.")]
|
||||
|
||||
[InlineData("{'type':9}", "Unknown message type: 9")]
|
||||
[InlineData("{'type':'foo'}", "Expected 'type' to be of type Integer.")]
|
||||
|
||||
[InlineData("{'type':3,'invocationId':'42','error':'foo','result':true}", "The 'error' and 'result' properties are mutually exclusive.")]
|
||||
|
|
@ -203,6 +202,19 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
Assert.Equal(expectedMessage, messages[0], TestHubMessageEqualityComparer.Instance);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{'type':1,'invocationId':'42','target':'foo','arguments':[],'extraParameter':'1'}")]
|
||||
public void ExtraItemsInMessageAreIgnored(string input)
|
||||
{
|
||||
input = Frame(input);
|
||||
|
||||
var binder = new TestBinder(paramTypes: new[] { typeof(int), typeof(string) }, returnType: typeof(bool));
|
||||
var protocol = new JsonHubProtocol();
|
||||
var messages = new List<HubMessage>();
|
||||
Assert.True(protocol.TryParseMessages(Encoding.UTF8.GetBytes(input), binder, messages));
|
||||
Assert.Single(messages);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{'type':1,'invocationId':'42','target':'foo','arguments':[]}", "Invocation provides 0 argument(s) but target expects 2.")]
|
||||
[InlineData("{'type':1,'arguments':[], 'invocationId':'42','target':'foo'}", "Invocation provides 0 argument(s) but target expects 2.")]
|
||||
|
|
|
|||
|
|
@ -293,6 +293,28 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
Assert.Equal(testData.Message, messages[0], TestHubMessageEqualityComparer.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseMessageWithExtraData()
|
||||
{
|
||||
var expectedMessage = new InvocationMessage(invocationId: "xyz", target: "method", argumentBindingException: null);
|
||||
var encodedObj = Array(HubProtocolConstants.InvocationMessageType, Map(), "xyz", "method", Array(), "ex");
|
||||
var binary = "lgGAo3h5eqZtZXRob2SQomV4";
|
||||
|
||||
// Verify that the input binary string decodes to the expected MsgPack primitives
|
||||
var bytes = Convert.FromBase64String(binary);
|
||||
var obj = Unpack(bytes);
|
||||
Assert.Equal(encodedObj, obj);
|
||||
|
||||
// Parse the input fully now.
|
||||
bytes = Frame(bytes);
|
||||
var protocol = new MessagePackHubProtocol();
|
||||
var messages = new List<HubMessage>();
|
||||
Assert.True(protocol.TryParseMessages(bytes, new TestBinder(expectedMessage), messages));
|
||||
|
||||
Assert.Single(messages);
|
||||
Assert.Equal(expectedMessage, messages[0], TestHubMessageEqualityComparer.Instance);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(TestDataNames))]
|
||||
public void WriteMessages(string testDataName)
|
||||
|
|
@ -315,7 +337,6 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
|
|||
{
|
||||
// Message Type
|
||||
new object[] { new InvalidMessageData("MessageTypeString", Array("foo"), "Reading 'messageType' as Int32 failed.") },
|
||||
new object[] { new InvalidMessageData("MessageTypeOutOfRange", Array(10), "Invalid message type: 10.") },
|
||||
|
||||
// Headers
|
||||
new object[] { new InvalidMessageData("HeadersNotAMap", Array(HubProtocolConstants.InvocationMessageType, "foo"), "Reading map length for 'headers' failed.") },
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
{
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
HandshakeProtocol.WriteRequestMessage(new HandshakeRequestMessage(_protocol.Name), memoryStream);
|
||||
HandshakeProtocol.WriteRequestMessage(new HandshakeRequestMessage(_protocol.Name, _protocol.Version), memoryStream);
|
||||
await Connection.Application.Output.WriteAsync(memoryStream.ToArray());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -370,6 +370,28 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandshakeFailureFromIncompatibleProtocolVersionSendsResponseWithError()
|
||||
{
|
||||
var hubProtocolMock = new Mock<IHubProtocol>();
|
||||
hubProtocolMock.Setup(m => m.Name).Returns("json");
|
||||
hubProtocolMock.Setup(m => m.Version).Returns(9001);
|
||||
|
||||
var endPoint = HubEndPointTestUtils.GetHubEndpoint(typeof(HubT));
|
||||
|
||||
using (var client = new TestClient(protocol: hubProtocolMock.Object))
|
||||
{
|
||||
var endPointTask = await client.ConnectAsync(endPoint);
|
||||
|
||||
Assert.NotNull(client.HandshakeResponseMessage);
|
||||
Assert.Equal("The server does not support version 9001 of the 'json' protocol.", client.HandshakeResponseMessage.Error);
|
||||
|
||||
client.Dispose();
|
||||
|
||||
await endPointTask.OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LifetimeManagerOnDisconnectedAsyncCalledIfLifetimeManagerOnConnectedAsyncThrows()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue