diff --git a/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/DefaultHubDispatcherBenchmark.cs b/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/DefaultHubDispatcherBenchmark.cs index 9137c79a5f..d21bb61c40 100644 --- a/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/DefaultHubDispatcherBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/DefaultHubDispatcherBenchmark.cs @@ -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 input, IInvocationBinder binder, IList messages) { return false; diff --git a/clients/ts/signalr-protocol-msgpack/package.json b/clients/ts/signalr-protocol-msgpack/package.json index f433edaa5e..66eab9fbad 100644 --- a/clients/ts/signalr-protocol-msgpack/package.json +++ b/clients/ts/signalr-protocol-msgpack/package.json @@ -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": [ diff --git a/clients/ts/signalr-protocol-msgpack/spec/MessagePackHubProtocol.spec.ts b/clients/ts/signalr-protocol-msgpack/spec/MessagePackHubProtocol.spec.ts index 2793466d7a..b76976c211 100644 --- a/clients/ts/signalr-protocol-msgpack/spec/MessagePackHubProtocol.spec.ts +++ b/clients/ts/signalr-protocol-msgpack/spec/MessagePackHubProtocol.spec.ts @@ -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, diff --git a/clients/ts/signalr-protocol-msgpack/src/MessagePackHubProtocol.ts b/clients/ts/signalr-protocol-msgpack/src/MessagePackHubProtocol.ts index 8e530ebff8..b19f916c7f 100644 --- a/clients/ts/signalr-protocol-msgpack/src/MessagePackHubProtocol.ts +++ b/clients/ts/signalr-protocol-msgpack/src/MessagePackHubProtocol.ts @@ -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."); } diff --git a/clients/ts/signalr/spec/HubConnection.spec.ts b/clients/ts/signalr/spec/HubConnection.spec.ts index d7d5ff5fbb..1695c09db6 100644 --- a/clients/ts/signalr/spec/HubConnection.spec.ts +++ b/clients/ts/signalr/spec/HubConnection.spec.ts @@ -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; diff --git a/clients/ts/signalr/spec/JsonHubProtocol.spec.ts b/clients/ts/signalr/spec/JsonHubProtocol.spec.ts new file mode 100644 index 0000000000..0bd154bab5 --- /dev/null +++ b/clients/ts/signalr/spec/JsonHubProtocol.spec.ts @@ -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, + }, + ]); + }); +}); diff --git a/clients/ts/signalr/src/HubConnection.ts b/clients/ts/signalr/src/HubConnection.ts index 3335b5eade..7750e8d7e1 100644 --- a/clients/ts/signalr/src/HubConnection.ts +++ b/clients/ts/signalr/src/HubConnection.ts @@ -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}'.`); diff --git a/clients/ts/signalr/src/IHubProtocol.ts b/clients/ts/signalr/src/IHubProtocol.ts index f339446a05..f42b7be42e 100644 --- a/clients/ts/signalr/src/IHubProtocol.ts +++ b/clients/ts/signalr/src/IHubProtocol.ts @@ -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; } diff --git a/clients/ts/signalr/src/JsonHubProtocol.ts b/clients/ts/signalr/src/JsonHubProtocol.ts index da6dd67736..b021b3a4b2 100644 --- a/clients/ts/signalr/src/JsonHubProtocol.ts +++ b/clients/ts/signalr/src/JsonHubProtocol.ts @@ -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); + } + } } diff --git a/specs/HubProtocol.md b/specs/HubProtocol.md index 4314713870..f7a49f028e 100644 --- a/specs/HubProtocol.md +++ b/specs/HubProtocol.md @@ -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: diff --git a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs index 130c7a141e..1b8c7bb35d 100644 --- a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs +++ b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs @@ -80,8 +80,8 @@ namespace Microsoft.AspNetCore.SignalR.Client private static readonly Action _receivedUnexpectedResponse = LoggerMessage.Define(LogLevel.Error, new EventId(23, "ReceivedUnexpectedResponse"), "Unsolicited response received for invocation '{InvocationId}'."); - private static readonly Action _hubProtocol = - LoggerMessage.Define(LogLevel.Information, new EventId(24, "HubProtocol"), "Using HubProtocol '{Protocol}'."); + private static readonly Action _hubProtocol = + LoggerMessage.Define(LogLevel.Information, new EventId(24, "HubProtocol"), "Using HubProtocol '{Protocol} v{Version}'."); private static readonly Action _preparingStreamingInvocation = LoggerMessage.Define(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) diff --git a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs index bf46b4598c..a5e9e90560 100644 --- a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs +++ b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs @@ -115,13 +115,13 @@ namespace Microsoft.AspNetCore.SignalR.Client _needKeepAlive = _connection.Features.Get() == 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); } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HandshakeProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HandshakeProtocol.cs index c9474852b0..bc6168be43 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HandshakeProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HandshakeProtocol.cs @@ -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(handshakeJObject, ProtocolPropertyName); - requestMessage = new HandshakeRequestMessage(protocol); + var protocolVersion = JsonUtils.GetRequiredProperty(handshakeJObject, ProtocolVersionName, JTokenType.Integer); + requestMessage = new HandshakeRequestMessage(protocol, protocolVersion); } return true; diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HandshakeRequestMessage.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HandshakeRequestMessage.cs index 1ee58b590c..f965d83ee4 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HandshakeRequestMessage.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HandshakeRequestMessage.cs @@ -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; } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/IHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/IHubProtocol.cs index b47f5dcf30..ab5f071eb5 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/IHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/IHubProtocol.cs @@ -12,10 +12,14 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol { string Name { get; } + int Version { get; } + TransferFormat TransferFormat { get; } bool TryParseMessages(ReadOnlyMemory input, IInvocationBinder binder, IList messages); void WriteMessage(HubMessage message, Stream output); + + bool IsVersionSupported(int version); } } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs index 447e35c588..c7522bf2fc 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs @@ -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 input, IInvocationBinder binder, IList 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); diff --git a/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs b/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs index e723d6adce..754bcfb4aa 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs @@ -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 _failedWritingMessage = LoggerMessage.Define(LogLevel.Debug, new EventId(6, "FailedWritingMessage"), "Failed writing message."); + private static readonly Action _protocolVersionFailed = + LoggerMessage.Define(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); + } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Protocols.MsgPack/Internal/Protocol/MessagePackHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Protocols.MsgPack/Internal/Protocol/MessagePackHubProtocol.cs index 8b01cbd3e3..07b84e923d 100644 --- a/src/Microsoft.AspNetCore.SignalR.Protocols.MsgPack/Internal/Protocol/MessagePackHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Protocols.MsgPack/Internal/Protocol/MessagePackHubProtocol.cs @@ -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 input, IInvocationBinder binder, IList 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; } } } diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs index dee8e3e2e6..c8453ccc58 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs @@ -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 { diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs index 03e4fcf1db..ff77652f34 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs @@ -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 input, IInvocationBinder binder, IList messages) { ParseCalls += 1; diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/HandshakeProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/HandshakeProtocolTests.cs index c4b993a0da..ab05298a79 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/HandshakeProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/HandshakeProtocolTests.cs @@ -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(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); diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs index 200d4cd515..1e63795e50 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs @@ -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(); + 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.")] diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs index dee4d528df..5253257b81 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs @@ -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(); + 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.") }, diff --git a/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs b/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs index c6958df9aa..d3c82332c0 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs @@ -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()); } } diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs b/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs index 698c87215c..7c44ffe8d4 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs @@ -370,6 +370,28 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } + [Fact] + public async Task HandshakeFailureFromIncompatibleProtocolVersionSendsResponseWithError() + { + var hubProtocolMock = new Mock(); + 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() {