Add protocol version to handshake (#1666)

This commit is contained in:
BrennanConroy 2018-03-22 15:54:35 -07:00 committed by GitHub
parent 6d642ea5ce
commit a47e1051e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 484 additions and 83 deletions

View File

@ -50,8 +50,14 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
public class FakeHubProtocol : IHubProtocol public class FakeHubProtocol : IHubProtocol
{ {
public string Name { get; } public string Name { get; }
public int Version => 1;
public TransferFormat TransferFormat { get; } public TransferFormat TransferFormat { get; }
public bool IsVersionSupported(int version)
{
return true;
}
public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages) public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
{ {
return false; return false;

View File

@ -18,7 +18,7 @@
"build:cjs": "node ../node_modules/typescript/bin/tsc --project ./tsconfig.json --module commonjs --outDir ./dist/cjs --target ES5", "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: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", "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" "test": "node ../node_modules/jasmine/bin/jasmine.js ./spec/obj/spec/**/*.spec.js"
}, },
"keywords": [ "keywords": [

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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"; import { MessagePackHubProtocol } from "../src/MessagePackHubProtocol";
describe("MessageHubProtocol", () => { describe("MessageHubProtocol", () => {
@ -14,7 +14,7 @@ describe("MessageHubProtocol", () => {
} as InvocationMessage; } as InvocationMessage;
const protocol = new MessagePackHubProtocol(); const protocol = new MessagePackHubProtocol();
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation)); const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), new NullLogger());
expect(parsedMessages).toEqual([invocation]); expect(parsedMessages).toEqual([invocation]);
}); });
@ -27,7 +27,7 @@ describe("MessageHubProtocol", () => {
} as InvocationMessage; } as InvocationMessage;
const protocol = new MessagePackHubProtocol(); const protocol = new MessagePackHubProtocol();
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation)); const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), new NullLogger());
expect(parsedMessages).toEqual([invocation]); expect(parsedMessages).toEqual([invocation]);
}); });
@ -42,7 +42,7 @@ describe("MessageHubProtocol", () => {
} as InvocationMessage; } as InvocationMessage;
const protocol = new MessagePackHubProtocol(); const protocol = new MessagePackHubProtocol();
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation)); const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), new NullLogger());
expect(parsedMessages).toEqual([invocation]); expect(parsedMessages).toEqual([invocation]);
}); });
@ -56,7 +56,7 @@ describe("MessageHubProtocol", () => {
} as InvocationMessage; } as InvocationMessage;
const protocol = new MessagePackHubProtocol(); const protocol = new MessagePackHubProtocol();
const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation)); const parsedMessages = protocol.parseMessages(protocol.writeMessage(invocation), new NullLogger());
expect(parsedMessages).toEqual([invocation]); expect(parsedMessages).toEqual([invocation]);
}); });
@ -93,9 +93,18 @@ describe("MessageHubProtocol", () => {
result: new Date(Date.UTC(2018, 0, 1, 11, 24, 0)), result: new Date(Date.UTC(2018, 0, 1, 11, 24, 0)),
type: MessageType.Completion, type: MessageType.Completion,
} as CompletionMessage], } 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]) => ] as Array<[number[], CompletionMessage]>).forEach(([payload, expectedMessage]) =>
it("can read Completion message", () => { 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]); expect(messages).toEqual([expectedMessage]);
})); }));
@ -113,10 +122,10 @@ describe("MessageHubProtocol", () => {
invocationId: "abc", invocationId: "abc",
item: new Date(Date.UTC(2018, 0, 1, 11, 24, 0)), item: new Date(Date.UTC(2018, 0, 1, 11, 24, 0)),
type: MessageType.StreamItem, type: MessageType.StreamItem,
} as StreamItemMessage] } as StreamItemMessage],
] as Array<[number[], StreamItemMessage]>).forEach(([payload, expectedMessage]) => ] as Array<[number[], StreamItemMessage]>).forEach(([payload, expectedMessage]) =>
it("can read StreamItem message", () => { 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]); expect(messages).toEqual([expectedMessage]);
})); }));
@ -132,7 +141,7 @@ describe("MessageHubProtocol", () => {
} as StreamItemMessage], } as StreamItemMessage],
] as Array<[number[], StreamItemMessage]>).forEach(([payload, expectedMessage]) => ] as Array<[number[], StreamItemMessage]>).forEach(([payload, expectedMessage]) =>
it("can read message with headers", () => { 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]); expect(messages).toEqual([expectedMessage]);
})); }));
@ -140,18 +149,15 @@ describe("MessageHubProtocol", () => {
["message with no payload", [0x00], new Error("Invalid payload.")], ["message with no payload", [0x00], new Error("Invalid payload.")],
["message with empty array", [0x01, 0x90], 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 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.")], ["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.")], ["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 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 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.")], ["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]) => ] as Array<[string, number[], Error]>).forEach(([name, payload, expectedError]) =>
it("throws for " + name, () => { it("throws for " + name, () => {
expect(() => new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer)) expect(() => new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer, new NullLogger()))
.toThrow(expectedError); .toThrow(expectedError);
})); }));
@ -159,7 +165,7 @@ describe("MessageHubProtocol", () => {
const payload = [ const payload = [
0x08, 0x94, 0x02, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x08, 0x08, 0x94, 0x02, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x08,
0x0b, 0x95, 0x03, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b]; 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([ expect(messages).toEqual([
{ {
headers: {}, headers: {},
@ -183,7 +189,7 @@ describe("MessageHubProtocol", () => {
0x91, // message array length = 1 (fixarray) 0x91, // message array length = 1 (fixarray)
0x06, // type = 6 = Ping (fixnum) 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([ expect(messages).toEqual([
{ {
type: MessageType.Ping, type: MessageType.Ping,

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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 { Buffer } from "buffer";
import * as msgpack5 from "msgpack5"; import * as msgpack5 from "msgpack5";
import { BinaryMessageFormat } from "./BinaryMessageFormat"; import { BinaryMessageFormat } from "./BinaryMessageFormat";
@ -9,14 +9,18 @@ import { BinaryMessageFormat } from "./BinaryMessageFormat";
export class MessagePackHubProtocol implements IHubProtocol { export class MessagePackHubProtocol implements IHubProtocol {
public readonly name: string = "messagepack"; public readonly name: string = "messagepack";
public readonly version: number = 1;
public readonly transferFormat: TransferFormat = TransferFormat.Binary; public readonly transferFormat: TransferFormat = TransferFormat.Binary;
public parseMessages(input: ArrayBuffer): HubMessage[] { public parseMessages(input: ArrayBuffer, logger: ILogger): HubMessage[] {
return BinaryMessageFormat.parse(input).map((m) => this.parseMessage(m)); 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) { if (input.length === 0) {
throw new Error("Invalid payload."); throw new Error("Invalid payload.");
} }
@ -41,12 +45,15 @@ export class MessagePackHubProtocol implements IHubProtocol {
case MessageType.Close: case MessageType.Close:
return this.createCloseMessage(properties); return this.createCloseMessage(properties);
default: 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 { 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."); throw new Error("Invalid payload for Close message.");
} }
@ -58,7 +65,8 @@ export class MessagePackHubProtocol implements IHubProtocol {
} }
private createPingMessage(properties: any[]): HubMessage { 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."); throw new Error("Invalid payload for Ping message.");
} }
@ -69,7 +77,8 @@ export class MessagePackHubProtocol implements IHubProtocol {
} }
private createInvocationMessage(headers: MessageHeaders, properties: any[]): InvocationMessage { 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."); throw new Error("Invalid payload for Invocation message.");
} }
@ -94,8 +103,9 @@ export class MessagePackHubProtocol implements IHubProtocol {
} }
private createStreamItemMessage(headers: MessageHeaders, properties: any[]): StreamItemMessage { private createStreamItemMessage(headers: MessageHeaders, properties: any[]): StreamItemMessage {
if (properties.length !== 4) { // check minimum length to allow protocol to add items to the end of objects in future releases
throw new Error("Invalid payload for stream Result message."); if (properties.length < 4) {
throw new Error("Invalid payload for StreamItem message.");
} }
return { return {
@ -107,6 +117,7 @@ export class MessagePackHubProtocol implements IHubProtocol {
} }
private createCompletionMessage(headers: MessageHeaders, properties: any[]): CompletionMessage { 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) { if (properties.length < 4) {
throw new Error("Invalid payload for Completion message."); throw new Error("Invalid payload for Completion message.");
} }
@ -117,8 +128,7 @@ export class MessagePackHubProtocol implements IHubProtocol {
const resultKind = properties[3]; const resultKind = properties[3];
if ((resultKind === voidResult && properties.length !== 4) || if (resultKind !== voidResult && properties.length < 5) {
(resultKind !== voidResult && properties.length !== 5)) {
throw new Error("Invalid payload for Completion message."); throw new Error("Invalid payload for Completion message.");
} }

View File

@ -4,7 +4,7 @@
import { ConnectionClosed, DataReceived } from "../src/Common"; import { ConnectionClosed, DataReceived } from "../src/Common";
import { HubConnection } from "../src/HubConnection"; import { HubConnection } from "../src/HubConnection";
import { IConnection } from "../src/IConnection"; 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 { ILogger, LogLevel } from "../src/ILogger";
import { Observer } from "../src/Observable"; import { Observer } from "../src/Observable";
import { TextMessageFormat } from "../src/TextMessageFormat"; import { TextMessageFormat } from "../src/TextMessageFormat";
@ -27,6 +27,7 @@ describe("HubConnection", () => {
expect(connection.sentData.length).toBe(1); expect(connection.sentData.length).toBe(1);
expect(JSON.parse(connection.sentData[0])).toEqual({ expect(JSON.parse(connection.sentData[0])).toEqual({
protocol: "json", protocol: "json",
version: 1,
}); });
await hubConnection.stop(); await hubConnection.stop();
}); });
@ -203,7 +204,7 @@ describe("HubConnection", () => {
const connection = new TestConnection(); const connection = new TestConnection();
const hubConnection = new HubConnection(connection, commonOptions); const hubConnection = new HubConnection(connection, commonOptions);
connection.receiveHandshakeResponse(); connection.receiveHandshakeResponse();
const invokePromise = hubConnection.invoke("testMethod"); const invokePromise = hubConnection.invoke("testMethod");
@ -232,7 +233,7 @@ describe("HubConnection", () => {
connection.receive({ connection.receive({
arguments: ["test"], arguments: ["test"],
invocationId: 0, invocationId: "0",
nonblocking: true, nonblocking: true,
target: "message", target: "message",
type: MessageType.Invocation, type: MessageType.Invocation,
@ -240,7 +241,7 @@ describe("HubConnection", () => {
expect(warnings).toEqual(["No client method with the name 'message' found."]); expect(warnings).toEqual(["No client method with the name 'message' found."]);
}); });
it("invocations ignored in callbacks that have registered then unregistered", async () => { it("invocations ignored in callbacks that have registered then unregistered", async () => {
const warnings: string[] = []; const warnings: string[] = [];
const logger = { const logger = {
@ -261,7 +262,7 @@ describe("HubConnection", () => {
connection.receive({ connection.receive({
arguments: ["test"], arguments: ["test"],
invocationId: 0, invocationId: "0",
nonblocking: true, nonblocking: true,
target: "message", target: "message",
type: MessageType.Invocation, type: MessageType.Invocation,
@ -273,7 +274,7 @@ describe("HubConnection", () => {
it("callback invoked when servers invokes a method on the client", async () => { it("callback invoked when servers invokes a method on the client", async () => {
const connection = new TestConnection(); const connection = new TestConnection();
const hubConnection = new HubConnection(connection, commonOptions); const hubConnection = new HubConnection(connection, commonOptions);
connection.receiveHandshakeResponse(); connection.receiveHandshakeResponse();
let value = ""; let value = "";
@ -281,7 +282,7 @@ describe("HubConnection", () => {
connection.receive({ connection.receive({
arguments: ["test"], arguments: ["test"],
invocationId: 0, invocationId: "0",
nonblocking: true, nonblocking: true,
target: "message", target: "message",
type: MessageType.Invocation, type: MessageType.Invocation,
@ -293,7 +294,7 @@ describe("HubConnection", () => {
it("stop on handshake error", async () => { it("stop on handshake error", async () => {
const connection = new TestConnection(); const connection = new TestConnection();
const hubConnection = new HubConnection(connection, commonOptions); const hubConnection = new HubConnection(connection, commonOptions);
let closeError: Error = null; let closeError: Error = null;
hubConnection.onclose((e) => closeError = e); hubConnection.onclose((e) => closeError = e);
@ -305,7 +306,7 @@ describe("HubConnection", () => {
it("stop on close message", async () => { it("stop on close message", async () => {
const connection = new TestConnection(); const connection = new TestConnection();
const hubConnection = new HubConnection(connection, commonOptions); const hubConnection = new HubConnection(connection, commonOptions);
let isClosed = false; let isClosed = false;
let closeError: Error = null; let closeError: Error = null;
hubConnection.onclose((e) => { hubConnection.onclose((e) => {
@ -326,7 +327,7 @@ describe("HubConnection", () => {
it("stop on error close message", async () => { it("stop on error close message", async () => {
const connection = new TestConnection(); const connection = new TestConnection();
const hubConnection = new HubConnection(connection, commonOptions); const hubConnection = new HubConnection(connection, commonOptions);
let isClosed = false; let isClosed = false;
let closeError: Error = null; let closeError: Error = null;
hubConnection.onclose((e) => { hubConnection.onclose((e) => {
@ -348,7 +349,7 @@ describe("HubConnection", () => {
it("can have multiple callbacks", async () => { it("can have multiple callbacks", async () => {
const connection = new TestConnection(); const connection = new TestConnection();
const hubConnection = new HubConnection(connection, commonOptions); const hubConnection = new HubConnection(connection, commonOptions);
connection.receiveHandshakeResponse(); connection.receiveHandshakeResponse();
let numInvocations1 = 0; let numInvocations1 = 0;
@ -358,7 +359,7 @@ describe("HubConnection", () => {
connection.receive({ connection.receive({
arguments: [], arguments: [],
invocationId: 0, invocationId: "0",
nonblocking: true, nonblocking: true,
target: "message", target: "message",
type: MessageType.Invocation, type: MessageType.Invocation,
@ -380,7 +381,7 @@ describe("HubConnection", () => {
connection.receive({ connection.receive({
arguments: [], arguments: [],
invocationId: 0, invocationId: "0",
nonblocking: true, nonblocking: true,
target: "message", target: "message",
type: MessageType.Invocation, type: MessageType.Invocation,
@ -390,7 +391,7 @@ describe("HubConnection", () => {
connection.receive({ connection.receive({
arguments: [], arguments: [],
invocationId: 0, invocationId: "0",
nonblocking: true, nonblocking: true,
target: "message", target: "message",
type: MessageType.Invocation, type: MessageType.Invocation,
@ -434,7 +435,7 @@ describe("HubConnection", () => {
// invoke a method to make sure we are not trying to use null/undefined // invoke a method to make sure we are not trying to use null/undefined
connection.receive({ connection.receive({
arguments: [], arguments: [],
invocationId: 0, invocationId: "0",
nonblocking: true, nonblocking: true,
target: "message", target: "message",
type: MessageType.Invocation, type: MessageType.Invocation,
@ -493,7 +494,7 @@ describe("HubConnection", () => {
it("completes the observer when a completion is received", async () => { it("completes the observer when a completion is received", async () => {
const connection = new TestConnection(); const connection = new TestConnection();
const hubConnection = new HubConnection(connection, commonOptions); const hubConnection = new HubConnection(connection, commonOptions);
connection.receiveHandshakeResponse(); connection.receiveHandshakeResponse();
const observer = new TestObserver(); const observer = new TestObserver();
@ -779,6 +780,7 @@ class TestConnection implements IConnection {
class TestProtocol implements IHubProtocol { class TestProtocol implements IHubProtocol {
public readonly name: string = "TestProtocol"; public readonly name: string = "TestProtocol";
public readonly version: number = 1;
public readonly transferFormat: TransferFormat; public readonly transferFormat: TransferFormat;

View File

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

View File

@ -70,7 +70,7 @@ export class HubConnection {
// Data may have all been read when processing handshake response // Data may have all been read when processing handshake response
if (data) { if (data) {
// Parse the messages // Parse the messages
const messages = this.protocol.parseMessages(data); const messages = this.protocol.parseMessages(data, this.logger);
for (const message of messages) { for (const message of messages) {
switch (message.type) { switch (message.type) {
@ -211,7 +211,7 @@ export class HubConnection {
// Handshake request is always JSON // Handshake request is always JSON
await this.connection.send( await this.connection.send(
TextMessageFormat.write( 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}'.`); this.logger.log(LogLevel.Information, `Using HubProtocol '${this.protocol.name}'.`);

View File

@ -1,4 +1,5 @@
import { TransferFormat } from "./Transports"; import { ILogger } from "./ILogger";
import { TransferFormat } from "./Transports";
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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 { export interface HandshakeRequestMessage {
readonly protocol: string; readonly protocol: string;
readonly version: number;
} }
export interface HandshakeResponseMessage { export interface HandshakeResponseMessage {
@ -72,7 +74,8 @@ export interface CancelInvocationMessage extends HubInvocationMessage {
export interface IHubProtocol { export interface IHubProtocol {
readonly name: string; readonly name: string;
readonly version: number;
readonly transferFormat: TransferFormat; readonly transferFormat: TransferFormat;
parseMessages(input: any): HubMessage[]; parseMessages(input: any, logger: ILogger): HubMessage[];
writeMessage(message: HubMessage): any; writeMessage(message: HubMessage): any;
} }

View File

@ -1,7 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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 { TextMessageFormat } from "./TextMessageFormat";
import { TransferFormat } from "./Transports"; import { TransferFormat } from "./Transports";
@ -10,19 +12,50 @@ export const JSON_HUB_PROTOCOL_NAME: string = "json";
export class JsonHubProtocol implements IHubProtocol { export class JsonHubProtocol implements IHubProtocol {
public readonly name: string = JSON_HUB_PROTOCOL_NAME; public readonly name: string = JSON_HUB_PROTOCOL_NAME;
public readonly version: number = 1;
public readonly transferFormat: TransferFormat = TransferFormat.Text; public readonly transferFormat: TransferFormat = TransferFormat.Text;
public parseMessages(input: string): HubMessage[] { public parseMessages(input: string, logger: ILogger): HubMessage[] {
if (!input) { if (!input) {
return []; return [];
} }
if (logger === null) {
logger = new NullLogger();
}
// Parse the messages // Parse the messages
const messages = TextMessageFormat.parse(input); const messages = TextMessageFormat.parse(input);
const hubMessages = []; const hubMessages = [];
for (const message of messages) { 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; return hubMessages;
@ -31,4 +64,38 @@ export class JsonHubProtocol implements IHubProtocol {
public writeMessage(message: HubMessage): string { public writeMessage(message: HubMessage): string {
return TextMessageFormat.write(JSON.stringify(message)); 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);
}
}
} }

View File

@ -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. | | `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. | | `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: 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 * `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: Example:
```json ```json
{ {
"protocol": "messagepack" "protocol": "messagepack",
"version": 1
} }
``` ```
The `HandshakeResponse` message contains the following properties: 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: Example:

View File

@ -80,8 +80,8 @@ namespace Microsoft.AspNetCore.SignalR.Client
private static readonly Action<ILogger, string, Exception> _receivedUnexpectedResponse = private static readonly Action<ILogger, string, Exception> _receivedUnexpectedResponse =
LoggerMessage.Define<string>(LogLevel.Error, new EventId(23, "ReceivedUnexpectedResponse"), "Unsolicited response received for invocation '{InvocationId}'."); LoggerMessage.Define<string>(LogLevel.Error, new EventId(23, "ReceivedUnexpectedResponse"), "Unsolicited response received for invocation '{InvocationId}'.");
private static readonly Action<ILogger, string, Exception> _hubProtocol = private static readonly Action<ILogger, string, int, Exception> _hubProtocol =
LoggerMessage.Define<string>(LogLevel.Information, new EventId(24, "HubProtocol"), "Using HubProtocol '{Protocol}'."); 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 = 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)."); 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); _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) public static void ResettingKeepAliveTimer(ILogger logger)

View File

@ -115,13 +115,13 @@ namespace Microsoft.AspNetCore.SignalR.Client
_needKeepAlive = _connection.Features.Get<IConnectionInherentKeepAliveFeature>() == null; _needKeepAlive = _connection.Features.Get<IConnectionInherentKeepAliveFeature>() == null;
_receivedHandshakeResponse = false; _receivedHandshakeResponse = false;
Log.HubProtocol(_logger, _protocol.Name); Log.HubProtocol(_logger, _protocol.Name, _protocol.Version);
_connectionActive = new CancellationTokenSource(); _connectionActive = new CancellationTokenSource();
using (var memoryStream = new MemoryStream()) using (var memoryStream = new MemoryStream())
{ {
Log.SendingHubHandshake(_logger); 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); await _connection.SendAsync(memoryStream.ToArray(), _connectionActive.Token);
} }

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections;
using System.IO; using System.IO;
using System.Text; using System.Text;
using Microsoft.AspNetCore.SignalR.Internal.Formatters; 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 static readonly UTF8Encoding _utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private const string ProtocolPropertyName = "protocol"; private const string ProtocolPropertyName = "protocol";
private const string ProtocolVersionName = "version";
private const string ErrorPropertyName = "error"; private const string ErrorPropertyName = "error";
private const string TypePropertyName = "type"; private const string TypePropertyName = "type";
@ -27,6 +27,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
writer.WriteStartObject(); writer.WriteStartObject();
writer.WritePropertyName(ProtocolPropertyName); writer.WritePropertyName(ProtocolPropertyName);
writer.WriteValue(requestMessage.Protocol); writer.WriteValue(requestMessage.Protocol);
writer.WritePropertyName(ProtocolVersionName);
writer.WriteValue(requestMessage.Version);
writer.WriteEndObject(); writer.WriteEndObject();
} }
@ -101,7 +103,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
var token = JToken.ReadFrom(reader); var token = JToken.ReadFrom(reader);
var handshakeJObject = JsonUtils.GetObject(token); var handshakeJObject = JsonUtils.GetObject(token);
var protocol = JsonUtils.GetRequiredProperty<string>(handshakeJObject, ProtocolPropertyName); 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; return true;

View File

@ -5,11 +5,13 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{ {
public class HandshakeRequestMessage : HubMessage public class HandshakeRequestMessage : HubMessage
{ {
public HandshakeRequestMessage(string protocol) public HandshakeRequestMessage(string protocol, int version)
{ {
Protocol = protocol; Protocol = protocol;
Version = version;
} }
public string Protocol { get; } public string Protocol { get; }
public int Version { get; }
} }
} }

View File

@ -12,10 +12,14 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{ {
string Name { get; } string Name { get; }
int Version { get; }
TransferFormat TransferFormat { get; } TransferFormat TransferFormat { get; }
bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages); bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages);
void WriteMessage(HubMessage message, Stream output); void WriteMessage(HubMessage message, Stream output);
bool IsVersionSupported(int version);
} }
} }

View File

@ -29,6 +29,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
private const string HeadersPropertyName = "headers"; private const string HeadersPropertyName = "headers";
public static readonly string ProtocolName = "json"; public static readonly string ProtocolName = "json";
public static readonly int ProtocolVersion = 1;
// ONLY to be used for application payloads (args, return values, etc.) // ONLY to be used for application payloads (args, return values, etc.)
public JsonSerializer PayloadSerializer { get; } public JsonSerializer PayloadSerializer { get; }
@ -44,14 +45,25 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
public string Name => ProtocolName; public string Name => ProtocolName;
public int Version => ProtocolVersion;
public TransferFormat TransferFormat => TransferFormat.Text; public TransferFormat TransferFormat => TransferFormat.Text;
public bool IsVersionSupported(int version)
{
return version == Version;
}
public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages) public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
{ {
while (TextMessageParser.TryParseMessage(ref input, out var payload)) while (TextMessageParser.TryParseMessage(ref input, out var payload))
{ {
var textReader = new Utf8BufferTextReader(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; return messages.Count > 0;
@ -277,7 +289,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
case null: case null:
throw new InvalidDataException($"Missing required property '{TypePropertyName}'."); throw new InvalidDataException($"Missing required property '{TypePropertyName}'.");
default: 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); return ApplyHeaders(message, headers);

View File

@ -239,6 +239,14 @@ namespace Microsoft.AspNetCore.SignalR
return false; 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'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 // 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. // what format we're writing.
@ -371,6 +379,9 @@ namespace Microsoft.AspNetCore.SignalR
private static readonly Action<ILogger, Exception> _failedWritingMessage = private static readonly Action<ILogger, Exception> _failedWritingMessage =
LoggerMessage.Define(LogLevel.Debug, new EventId(6, "FailedWritingMessage"), "Failed writing message."); 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) public static void HandshakeComplete(ILogger logger, string hubProtocol)
{ {
_handshakeComplete(logger, hubProtocol, null); _handshakeComplete(logger, hubProtocol, null);
@ -400,6 +411,11 @@ namespace Microsoft.AspNetCore.SignalR
{ {
_failedWritingMessage(logger, exception); _failedWritingMessage(logger, exception);
} }
public static void ProtocolVersionFailed(ILogger logger, string protocolName, int version)
{
_protocolVersionFailed(logger, protocolName, version, null);
}
} }
} }

View File

@ -9,7 +9,6 @@ using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Protocols; using Microsoft.AspNetCore.Protocols;
using Microsoft.AspNetCore.SignalR.Internal.Formatters; using Microsoft.AspNetCore.SignalR.Internal.Formatters;
using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MsgPack; using MsgPack;
using MsgPack.Serialization; using MsgPack.Serialization;
@ -23,11 +22,14 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
private const int NonVoidResult = 3; private const int NonVoidResult = 3;
public static readonly string ProtocolName = "messagepack"; public static readonly string ProtocolName = "messagepack";
public static readonly int ProtocolVersion = 1;
public SerializationContext SerializationContext { get; } public SerializationContext SerializationContext { get; }
public string Name => ProtocolName; public string Name => ProtocolName;
public int Version => ProtocolVersion;
public TransferFormat TransferFormat => TransferFormat.Binary; public TransferFormat TransferFormat => TransferFormat.Binary;
public MessagePackHubProtocol() public MessagePackHubProtocol()
@ -39,6 +41,11 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
SerializationContext = options.Value.SerializationContext; SerializationContext = options.Value.SerializationContext;
} }
public bool IsVersionSupported(int version)
{
return version == Version;
}
public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages) public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
{ {
while (BinaryMessageParser.TryParseMessage(ref input, out var payload)) 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); var isArray = MemoryMarshal.TryGetArray(payload, out var arraySegment);
// This will never be false unless we started using un-managed buffers // This will never be false unless we started using un-managed buffers
Debug.Assert(isArray); 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; return messages.Count > 0;
@ -77,7 +88,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
case HubProtocolConstants.CloseMessageType: case HubProtocolConstants.CloseMessageType:
return CreateCloseMessage(unpacker); return CreateCloseMessage(unpacker);
default: default:
throw new FormatException($"Invalid message type: {messageType}."); // Future protocol changes can add message types, old clients can ignore them
return null;
} }
} }
} }

View File

@ -2,15 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.SignalR.Client.Tests namespace Microsoft.AspNetCore.SignalR.Client.Tests
@ -55,7 +50,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var handshakeMessage = await connection.ReadSentTextMessageAsync().OrTimeout(); var handshakeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
Assert.Equal("{\"protocol\":\"json\"}\u001e", handshakeMessage); Assert.Equal("{\"protocol\":\"json\",\"version\":1}\u001e", handshakeMessage);
} }
finally finally
{ {

View File

@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Protocols; using Microsoft.AspNetCore.Protocols;
using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Internal.Protocol;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Client; using Microsoft.AspNetCore.Sockets.Client;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moq; using Moq;
@ -252,9 +251,15 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
} }
public string Name => "MockHubProtocol"; public string Name => "MockHubProtocol";
public int Version => 1;
public TransferFormat TransferFormat => TransferFormat.Binary; public TransferFormat TransferFormat => TransferFormat.Binary;
public bool IsVersionSupported(int version)
{
return true;
}
public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages) public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
{ {
ParseCalls += 1; ParseCalls += 1;

View File

@ -13,16 +13,17 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
public class HandshakeProtocolTests public class HandshakeProtocolTests
{ {
[Theory] [Theory]
[InlineData("{\"protocol\":\"dummy\"}\u001e", "dummy")] [InlineData("{\"protocol\":\"dummy\",\"version\":1}\u001e", "dummy", 1)]
[InlineData("{\"protocol\":\"\"}\u001e", "")] [InlineData("{\"protocol\":\"\",\"version\":10}\u001e", "", 10)]
[InlineData("{\"protocol\":null}\u001e", null)] [InlineData("{\"protocol\":null,\"version\":123}\u001e", null, 123)]
public void ParsingHandshakeRequestMessageSuccessForValidMessages(string json, string protocol) public void ParsingHandshakeRequestMessageSuccessForValidMessages(string json, string protocol, int version)
{ {
var message = Encoding.UTF8.GetBytes(json); var message = Encoding.UTF8.GetBytes(json);
Assert.True(HandshakeProtocol.TryParseRequestMessage(new ReadOnlySequence<byte>(message), out var deserializedMessage, out _, out _)); Assert.True(HandshakeProtocol.TryParseRequestMessage(new ReadOnlySequence<byte>(message), out var deserializedMessage, out _, out _));
Assert.Equal(protocol, deserializedMessage.Protocol); Assert.Equal(protocol, deserializedMessage.Protocol);
Assert.Equal(version, deserializedMessage.Version);
} }
[Theory] [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("null\u001e", "Unexpected JSON Token Type 'Null'. Expected a JSON Object.")]
[InlineData("{}\u001e", "Missing required property 'protocol'.")] [InlineData("{}\u001e", "Missing required property 'protocol'.")]
[InlineData("[]\u001e", "Unexpected JSON Token Type 'Array'. Expected a JSON Object.")] [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) public void ParsingHandshakeRequestMessageThrowsForInvalidMessages(string payload, string expectedMessage)
{ {
var message = Encoding.UTF8.GetBytes(payload); var message = Encoding.UTF8.GetBytes(payload);

View File

@ -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'}", "Missing required property 'arguments'.")]
[InlineData("{'type':4,'invocationId':'42','target':'foo','arguments':{}}", "Expected 'arguments' to be of type Array.")] [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':'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.")] [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); 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] [Theory]
[InlineData("{'type':1,'invocationId':'42','target':'foo','arguments':[]}", "Invocation provides 0 argument(s) but target expects 2.")] [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.")] [InlineData("{'type':1,'arguments':[], 'invocationId':'42','target':'foo'}", "Invocation provides 0 argument(s) but target expects 2.")]

View File

@ -293,6 +293,28 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
Assert.Equal(testData.Message, messages[0], TestHubMessageEqualityComparer.Instance); 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] [Theory]
[MemberData(nameof(TestDataNames))] [MemberData(nameof(TestDataNames))]
public void WriteMessages(string testDataName) public void WriteMessages(string testDataName)
@ -315,7 +337,6 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
{ {
// Message Type // Message Type
new object[] { new InvalidMessageData("MessageTypeString", Array("foo"), "Reading 'messageType' as Int32 failed.") }, new object[] { new InvalidMessageData("MessageTypeString", Array("foo"), "Reading 'messageType' as Int32 failed.") },
new object[] { new InvalidMessageData("MessageTypeOutOfRange", Array(10), "Invalid message type: 10.") },
// Headers // Headers
new object[] { new InvalidMessageData("HeadersNotAMap", Array(HubProtocolConstants.InvocationMessageType, "foo"), "Reading map length for 'headers' failed.") }, new object[] { new InvalidMessageData("HeadersNotAMap", Array(HubProtocolConstants.InvocationMessageType, "foo"), "Reading map length for 'headers' failed.") },

View File

@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
{ {
using (var memoryStream = new MemoryStream()) 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()); await Connection.Application.Output.WriteAsync(memoryStream.ToArray());
} }
} }

View File

@ -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] [Fact]
public async Task LifetimeManagerOnDisconnectedAsyncCalledIfLifetimeManagerOnConnectedAsyncThrows() public async Task LifetimeManagerOnDisconnectedAsyncCalledIfLifetimeManagerOnConnectedAsyncThrows()
{ {