diff --git a/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/BroadcastBenchmark.cs b/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/BroadcastBenchmark.cs index 46073813f9..042b5034ed 100644 --- a/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/BroadcastBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/BroadcastBenchmark.cs @@ -5,7 +5,6 @@ using System.Threading.Channels; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.SignalR.Internal; -using Microsoft.AspNetCore.SignalR.Internal.Encoders; using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.Sockets; using Microsoft.Extensions.Logging.Abstractions; @@ -40,14 +39,12 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks protocol = new MessagePackHubProtocol(); } - var encoder = new PassThroughEncoder(); - for (var i = 0; i < Connections; ++i) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var connection = new DefaultConnectionContext(Guid.NewGuid().ToString(), pair.Application, pair.Transport); var hubConnection = new HubConnectionContext(connection, Timeout.InfiniteTimeSpan, NullLoggerFactory.Instance); - hubConnection.ProtocolReaderWriter = new HubProtocolReaderWriter(protocol, encoder); + hubConnection.Protocol = protocol; _hubLifetimeManager.OnConnectedAsync(hubConnection).Wait(); } diff --git a/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/DefaultHubDispatcherBenchmark.cs b/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/DefaultHubDispatcherBenchmark.cs index 8676407059..e15fbe86dd 100644 --- a/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/DefaultHubDispatcherBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/DefaultHubDispatcherBenchmark.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -11,12 +11,11 @@ using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Protocols; using Microsoft.AspNetCore.SignalR.Internal; -using Microsoft.AspNetCore.SignalR.Internal.Encoders; using Microsoft.AspNetCore.SignalR.Internal.Protocol; +using Microsoft.AspNetCore.Sockets; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Moq; using DefaultConnectionContext = Microsoft.AspNetCore.Sockets.DefaultConnectionContext; namespace Microsoft.AspNetCore.SignalR.Microbenchmarks @@ -47,13 +46,13 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks _connectionContext = new NoErrorHubConnectionContext(connection, TimeSpan.Zero, NullLoggerFactory.Instance); - _connectionContext.ProtocolReaderWriter = new HubProtocolReaderWriter(new FakeHubProtocol(), new FakeDataEncoder()); + _connectionContext.Protocol = new FakeHubProtocol(); } public class FakeHubProtocol : IHubProtocol { public string Name { get; } - public ProtocolType Type { get; } + public TransferFormat TransferFormat { get; } public bool TryParseMessages(ReadOnlySpan input, IInvocationBinder binder, IList messages) { @@ -65,19 +64,6 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks } } - public class FakeDataEncoder : IDataEncoder - { - public byte[] Encode(byte[] payload) - { - return null; - } - - public bool TryDecode(ref ReadOnlySpan buffer, out ReadOnlySpan data) - { - return false; - } - } - public class NoErrorHubConnectionContext : HubConnectionContext { public NoErrorHubConnectionContext(ConnectionContext connectionContext, TimeSpan keepAliveInterval, ILoggerFactory loggerFactory) : base(connectionContext, keepAliveInterval, loggerFactory) @@ -232,4 +218,4 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks return _dispatcher.DispatchMessageAsync(_connectionContext, new StreamInvocationMessage("123", "StreamChannelReaderValueTaskAsync", null)); } } -} \ No newline at end of file +} diff --git a/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/HubProtocolBenchmark.cs b/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/HubProtocolBenchmark.cs index 8ee96f8d9c..5d8468a609 100644 --- a/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/HubProtocolBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/HubProtocolBenchmark.cs @@ -2,16 +2,17 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.IO; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.SignalR.Internal; -using Microsoft.AspNetCore.SignalR.Internal.Encoders; using Microsoft.AspNetCore.SignalR.Internal.Protocol; namespace Microsoft.AspNetCore.SignalR.Microbenchmarks { public class HubProtocolBenchmark { - private HubProtocolReaderWriter _hubProtocolReaderWriter; + private IHubProtocol _hubProtocol; private byte[] _binaryInput; private TestBinder _binder; private HubMessage _hubMessage; @@ -28,10 +29,10 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks switch (HubProtocol) { case Protocol.MsgPack: - _hubProtocolReaderWriter = new HubProtocolReaderWriter(new MessagePackHubProtocol(), new PassThroughEncoder()); + _hubProtocol = new MessagePackHubProtocol(); break; case Protocol.Json: - _hubProtocolReaderWriter = new HubProtocolReaderWriter(new JsonHubProtocol(), new PassThroughEncoder()); + _hubProtocol = new JsonHubProtocol(); break; } @@ -51,14 +52,15 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks break; } - _binaryInput = GetBytes(_hubMessage); + _binaryInput = _hubProtocol.WriteToArray(_hubMessage); _binder = new TestBinder(_hubMessage); } [Benchmark] public void ReadSingleMessage() { - if (!_hubProtocolReaderWriter.ReadMessages(_binaryInput, _binder, out var _)) + var messages = new List(); + if (!_hubProtocol.TryParseMessages(_binaryInput, _binder, messages)) { throw new InvalidOperationException("Failed to read message"); } @@ -67,7 +69,8 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks [Benchmark] public void WriteSingleMessage() { - if (_hubProtocolReaderWriter.WriteMessage(_hubMessage).Length != _binaryInput.Length) + var bytes = _hubProtocol.WriteToArray(_hubMessage); + if (bytes.Length != _binaryInput.Length) { throw new InvalidOperationException("Failed to write message"); } @@ -86,10 +89,5 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks ManyArguments = 2, LargeArguments = 3 } - - private byte[] GetBytes(HubMessage hubMessage) - { - return _hubProtocolReaderWriter.WriteMessage(_hubMessage); - } } } diff --git a/client-ts/FunctionalTests/ts/HubConnectionTests.ts b/client-ts/FunctionalTests/ts/HubConnectionTests.ts index 1e77f66ffb..0607926f99 100644 --- a/client-ts/FunctionalTests/ts/HubConnectionTests.ts +++ b/client-ts/FunctionalTests/ts/HubConnectionTests.ts @@ -521,6 +521,42 @@ describe("hubConnection", () => { }); }); + if (typeof EventSource !== "undefined") { + it("allows Server-Sent Events when negotiating for JSON protocol", async (done) => { + const hubConnection = new HubConnection(TESTHUB_NOWEBSOCKETS_ENDPOINT_URL, { + logger: LogLevel.Trace, + protocol: new JsonHubProtocol(), + }); + + try { + await hubConnection.start(); + + // Check what transport was used by asking the server to tell us. + expect(await hubConnection.invoke("GetActiveTransportName")).toEqual("ServerSentEvents"); + done(); + } catch (e) { + fail(e); + } + }); + } + + it("skips Server-Sent Events when negotiating for MsgPack protocol", async (done) => { + const hubConnection = new HubConnection(TESTHUB_NOWEBSOCKETS_ENDPOINT_URL, { + logger: LogLevel.Trace, + protocol: new MessagePackHubProtocol(), + }); + + try { + await hubConnection.start(); + + // Check what transport was used by asking the server to tell us. + expect(await hubConnection.invoke("GetActiveTransportName")).toEqual("LongPolling"); + done(); + } catch (e) { + fail(e); + } + }); + function getJwtToken(url): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); diff --git a/client-ts/FunctionalTests/web.config b/client-ts/FunctionalTests/web.config index dc0514fca5..8700b60c05 100644 --- a/client-ts/FunctionalTests/web.config +++ b/client-ts/FunctionalTests/web.config @@ -1,14 +1,12 @@  - - - + - + - + \ No newline at end of file diff --git a/client-ts/signalr-protocol-msgpack/package-lock.json b/client-ts/signalr-protocol-msgpack/package-lock.json index 532a81cc67..65b128af7f 100644 --- a/client-ts/signalr-protocol-msgpack/package-lock.json +++ b/client-ts/signalr-protocol-msgpack/package-lock.json @@ -1,9 +1,18 @@ { "name": "@aspnet/signalr-protocol-msgpack", - "version": "1.0.0-preview2-t000", + "version": "1.0.0-preview1-t000", "lockfileVersion": 1, "requires": true, "dependencies": { + "@aspnet/signalr": { + "version": "file:../signalr", + "dependencies": { + "es6-promise": { + "version": "4.2.2", + "bundled": true + } + } + }, "@types/bl": { "version": "0.8.31", "resolved": "https://registry.npmjs.org/@types/bl/-/bl-0.8.31.tgz", diff --git a/client-ts/signalr-protocol-msgpack/package.json b/client-ts/signalr-protocol-msgpack/package.json index f47ca4f2f7..5878624fbf 100644 --- a/client-ts/signalr-protocol-msgpack/package.json +++ b/client-ts/signalr-protocol-msgpack/package.json @@ -1,6 +1,6 @@ { "name": "@aspnet/signalr-protocol-msgpack", - "version": "1.0.0-preview1-t000", + "version": "1.0.0-preview2-t000", "description": "MsgPack Protocol support for ASP.NET Core SignalR", "main": "./dist/cjs/index.js", "browser": "./dist/browser/signalr-protocol-msgpack.js", @@ -18,8 +18,8 @@ "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", - "test": "node ../node_modules/jasmine/bin/jasmine.js ./spec/obj/signalr-protocol-msgpack/spec/**/*.spec.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", + "test": "node ../node_modules/jasmine/bin/jasmine.js ./spec/obj/spec/**/*.spec.js" }, "keywords": [ "signalr", diff --git a/client-ts/signalr-protocol-msgpack/spec/tsconfig.json b/client-ts/signalr-protocol-msgpack/spec/tsconfig.json index 727d660066..aa378b5da3 100644 --- a/client-ts/signalr-protocol-msgpack/spec/tsconfig.json +++ b/client-ts/signalr-protocol-msgpack/spec/tsconfig.json @@ -8,7 +8,7 @@ "lib": [ "es2015", "dom" ], "baseUrl": ".", "paths": { - "@aspnet/signalr": [ "../../signalr/src/index" ] + "@aspnet/*": [ "../../*" ] } }, "include": [ diff --git a/client-ts/signalr-protocol-msgpack/src/MessagePackHubProtocol.ts b/client-ts/signalr-protocol-msgpack/src/MessagePackHubProtocol.ts index 4e0b6ec18d..536a64bc44 100644 --- a/client-ts/signalr-protocol-msgpack/src/MessagePackHubProtocol.ts +++ b/client-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, ProtocolType, StreamInvocationMessage, StreamItemMessage } from "@aspnet/signalr"; +import { CompletionMessage, HubMessage, IHubProtocol, InvocationMessage, MessageHeaders, MessageType, StreamInvocationMessage, StreamItemMessage, TransferFormat } from "@aspnet/signalr"; import { Buffer } from "buffer"; import * as msgpack5 from "msgpack5"; import { BinaryMessageFormat } from "./BinaryMessageFormat"; @@ -10,7 +10,7 @@ export class MessagePackHubProtocol implements IHubProtocol { public readonly name: string = "messagepack"; - public readonly type: ProtocolType = ProtocolType.Binary; + public readonly transferFormat: TransferFormat = TransferFormat.Binary; public parseMessages(input: ArrayBuffer): HubMessage[] { return BinaryMessageFormat.parse(input).map((m) => this.parseMessage(m)); diff --git a/client-ts/signalr/package-lock.json b/client-ts/signalr/package-lock.json index 264748f026..49d499908f 100644 --- a/client-ts/signalr/package-lock.json +++ b/client-ts/signalr/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aspnet/signalr", - "version": "1.0.0-preview2-t000", + "version": "1.0.0-preview1-t000", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/client-ts/signalr/package.json b/client-ts/signalr/package.json index 8f1b8bdb02..81fc9ef809 100644 --- a/client-ts/signalr/package.json +++ b/client-ts/signalr/package.json @@ -1,6 +1,6 @@ { "name": "@aspnet/signalr", - "version": "1.0.0-preview1-t000", + "version": "1.0.0-preview2-t000", "description": "ASP.NET Core SignalR Client", "main": "./dist/cjs/index.js", "browser": "./dist/browser/signalr.js", diff --git a/client-ts/signalr/spec/Base64EncodedHubProtocol.spec.ts b/client-ts/signalr/spec/Base64EncodedHubProtocol.spec.ts deleted file mode 100644 index 14a376bf5a..0000000000 --- a/client-ts/signalr/spec/Base64EncodedHubProtocol.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -// 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 { Base64EncodedHubProtocol } from "../src/Base64EncodedHubProtocol"; -import { HubMessage, IHubProtocol, ProtocolType } from "../src/IHubProtocol"; - -class FakeHubProtocol implements IHubProtocol { - public name: "fakehubprotocol"; - public type: ProtocolType; - - public parseMessages(input: any): HubMessage[] { - let s = ""; - - new Uint8Array(input).forEach((item: any) => { - s += String.fromCharCode(item); - }); - - return JSON.parse(s); - } - - public writeMessage(message: HubMessage): any { - const s = JSON.stringify(message); - const payload = new Uint8Array(s.length); - for (let i = 0; i < payload.length; i++) { - payload[i] = s.charCodeAt(i); - } - return payload; - } -} - -describe("Base64EncodedHubProtocol", () => { - ([ - ["ABC", new Error("Invalid payload.")], - ["3:ABC", new Error("Invalid payload.")], - [":;", new Error("Invalid length: ''")], - ["1.0:A;", new Error("Invalid length: '1.0'")], - ["2:A;", new Error("Invalid message size.")], - ["2:ABC;", new Error("Invalid message size.")], - ] as Array<[string, Error]>).forEach(([payload, expectedError]) => { - it(`should fail to parse '${payload}'`, () => { - expect(() => new Base64EncodedHubProtocol(new FakeHubProtocol()).parseMessages(payload)).toThrow(expectedError); - }); - }); - - ([ - ["2:{};", {}], - ] as [[string, any]]).forEach(([payload, message]) => { - it(`should be able to parse '${payload}'`, () => { - - const globalAny: any = global; - globalAny.atob = (input: any) => input; - - const result = new Base64EncodedHubProtocol(new FakeHubProtocol()).parseMessages(payload); - expect(result).toEqual(message); - - delete globalAny.atob; - }); - }); - - ([ - [{}, "2:{};"], - ] as Array<[any, string]>).forEach(([message, payload]) => { - it(`should be able to write '${JSON.stringify(message)}'`, () => { - - const globalAny: any = global; - globalAny.btoa = (input: any) => input; - - const result = new Base64EncodedHubProtocol(new FakeHubProtocol()).writeMessage(message); - expect(result).toEqual(payload); - - delete globalAny.btoa; - }); - }); -}); diff --git a/client-ts/signalr/spec/HttpConnection.spec.ts b/client-ts/signalr/spec/HttpConnection.spec.ts index 2d24100934..79d0acfcf9 100644 --- a/client-ts/signalr/spec/HttpConnection.spec.ts +++ b/client-ts/signalr/spec/HttpConnection.spec.ts @@ -5,7 +5,7 @@ import { DataReceived, TransportClosed } from "../src/Common"; import { HttpConnection } from "../src/HttpConnection"; import { IHttpConnectionOptions } from "../src/HttpConnection"; import { HttpResponse } from "../src/index"; -import { ITransport, TransferMode, TransportType } from "../src/Transports"; +import { ITransport, TransferFormat, TransportType } from "../src/Transports"; import { eachEndpointUrl, eachTransport } from "./Common"; import { TestHttpClient } from "./TestHttpClient"; @@ -15,13 +15,13 @@ const commonOptions: IHttpConnectionOptions = { describe("HttpConnection", () => { it("cannot be created with relative url if document object is not present", () => { - expect(() => new HttpConnection("/test", commonOptions)) + expect(() => new HttpConnection("/test", TransferFormat.Text, commonOptions)) .toThrow(new Error("Cannot resolve '/test'.")); }); it("cannot be created with relative url if window object is not present", () => { (global as any).window = {}; - expect(() => new HttpConnection("/test", commonOptions)) + expect(() => new HttpConnection("/test", TransferFormat.Text, commonOptions)) .toThrow(new Error("Cannot resolve '/test'.")); delete (global as any).window; }); @@ -34,7 +34,7 @@ describe("HttpConnection", () => { .on("GET", (r) => ""), } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); + const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options); try { await connection.start(); @@ -64,7 +64,7 @@ describe("HttpConnection", () => { }), } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); + const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options); try { await connection.start(); @@ -86,7 +86,7 @@ describe("HttpConnection", () => { .on("GET", (r) => ""), } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); + const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options); try { await connection.start(); @@ -117,7 +117,7 @@ describe("HttpConnection", () => { }), } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); + const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options); try { await connection.start(); @@ -129,17 +129,17 @@ describe("HttpConnection", () => { }); it("can stop a non-started connection", async (done) => { - const connection = new HttpConnection("http://tempuri.org", commonOptions); + const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, commonOptions); await connection.stop(); done(); }); - it("preserves users connection string", async (done) => { + it("preserves user's query string", async (done) => { let connectUrl: string; const fakeTransport: ITransport = { - connect(url: string): Promise { + connect(url: string): Promise { connectUrl = url; - return Promise.reject(TransferMode.Text); + return Promise.reject(""); }, send(data: any): Promise { return Promise.reject(""); @@ -159,7 +159,7 @@ describe("HttpConnection", () => { transport: fakeTransport, } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org?q=myData", options); + const connection = new HttpConnection("http://tempuri.org?q=myData", TransferFormat.Text, options); try { await connection.start(); @@ -190,7 +190,7 @@ describe("HttpConnection", () => { }), } as IHttpConnectionOptions; - connection = new HttpConnection(givenUrl, options); + connection = new HttpConnection(givenUrl, TransferFormat.Text, options); try { await connection.start(); @@ -213,18 +213,18 @@ describe("HttpConnection", () => { const options: IHttpConnectionOptions = { ...commonOptions, httpClient: new TestHttpClient() - .on("POST", (r) => "{ \"connectionId\": \"42\", \"availableTransports\": [] }") + .on("POST", (r) => ({ connectionId: "42", availableTransports: [] })) .on("GET", (r) => ""), transport: requestedTransport, } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); + const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options); try { await connection.start(); fail(); done(); } catch (e) { - expect(e.message).toBe("No available transports found."); + expect(e.message).toBe("Unable to initialize any of the available transports."); done(); } }); @@ -234,17 +234,17 @@ describe("HttpConnection", () => { const options: IHttpConnectionOptions = { ...commonOptions, httpClient: new TestHttpClient() - .on("POST", (r) => "{ \"connectionId\": \"42\", \"availableTransports\": [] }") + .on("POST", (r) => ({ connectionId: "42", availableTransports: [] })) .on("GET", (r) => ""), } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); + const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options); try { await connection.start(); fail(); done(); } catch (e) { - expect(e.message).toBe("No available transports found."); + expect(e.message).toBe("Unable to initialize any of the available transports."); done(); } }); @@ -256,7 +256,7 @@ describe("HttpConnection", () => { transport: TransportType.WebSockets, } as IHttpConnectionOptions; - const connection = new HttpConnection("http://tempuri.org", options); + const connection = new HttpConnection("http://tempuri.org", TransferFormat.Text, options); try { await connection.start(); fail(); @@ -264,42 +264,24 @@ describe("HttpConnection", () => { } catch (e) { // WebSocket is created when the transport is connecting which happens after // negotiate request would be sent. No better/easier way to test this. - expect(e.message).toBe("WebSocket is not defined"); + expect(e.message).toBe("'WebSocket' is not supported in your environment."); done(); } }); - [ - [TransferMode.Text, TransferMode.Text], - [TransferMode.Text, TransferMode.Binary], - [TransferMode.Binary, TransferMode.Text], - [TransferMode.Binary, TransferMode.Binary], - ].forEach(([requestedTransferMode, transportTransferMode]) => { - it(`connection returns ${transportTransferMode} transfer mode when ${requestedTransferMode} transfer mode is requested`, async () => { - const fakeTransport = { - // mode: TransferMode : TransferMode.Text - connect(url: string, requestedTransferMode: TransferMode): Promise { return Promise.resolve(transportTransferMode); }, - mode: transportTransferMode, - onclose: null, - onreceive: null, - send(data: any): Promise { return Promise.resolve(); }, - stop(): Promise { return Promise.resolve(); }, - } as ITransport; + describe(".constructor", () => { + it("throws if no Url is provided", async () => { + // Force TypeScript to let us call the constructor incorrectly :) + expect(() => new (HttpConnection as any)()).toThrowError("The 'url' argument is required."); + }); - const options: IHttpConnectionOptions = { - ...commonOptions, - httpClient: new TestHttpClient() - .on("POST", (r) => "{ \"connectionId\": \"42\", \"availableTransports\": [] }") - .on("GET", (r) => ""), - transport: fakeTransport, - } as IHttpConnectionOptions; + it("throws if no TransferFormat is provided", async () => { + // Force TypeScript to let us call the constructor incorrectly :) + expect(() => new (HttpConnection as any)("http://tempuri.org")).toThrowError("The 'transferFormat' argument is required."); + }); - const connection = new HttpConnection("https://tempuri.org", options); - connection.features.transferMode = requestedTransferMode; - await connection.start(); - const actualTransferMode = connection.features.transferMode; - - expect(actualTransferMode).toBe(transportTransferMode); + it("throws if an unsupported TransferFormat is provided", async () => { + expect(() => new HttpConnection("http://tempuri.org", 42)).toThrowError("Unknown transferFormat value: 42."); }); }); }); diff --git a/client-ts/signalr/spec/HubConnection.spec.ts b/client-ts/signalr/spec/HubConnection.spec.ts index bb709bb2dc..01cf87e8e4 100644 --- a/client-ts/signalr/spec/HubConnection.spec.ts +++ b/client-ts/signalr/spec/HubConnection.spec.ts @@ -8,7 +8,7 @@ import { MessageType } from "../src/IHubProtocol"; import { ILogger, LogLevel } from "../src/ILogger"; import { Observer } from "../src/Observable"; import { TextMessageFormat } from "../src/TextMessageFormat"; -import { ITransport, TransferMode, TransportType } from "../src/Transports"; +import { ITransport, TransferFormat, TransportType } from "../src/Transports"; import { IHubConnectionOptions } from "../src/HubConnection"; import { asyncit as it, captureException, delay, PromiseSource } from "./Utils"; diff --git a/client-ts/signalr/spec/TestHttpClient.ts b/client-ts/signalr/spec/TestHttpClient.ts index 0f879cbde3..90194f2efc 100644 --- a/client-ts/signalr/spec/TestHttpClient.ts +++ b/client-ts/signalr/spec/TestHttpClient.ts @@ -3,7 +3,7 @@ import { HttpClient, HttpRequest, HttpResponse } from "../src/HttpClient"; -type TestHttpHandlerResult = HttpResponse | string; +type TestHttpHandlerResult = any; export type TestHttpHandler = (request: HttpRequest, next?: (request: HttpRequest) => Promise) => Promise | TestHttpHandlerResult; export class TestHttpClient extends HttpClient { @@ -57,9 +57,14 @@ export class TestHttpClient extends HttpClient { } if (typeof val === "string") { + // string payload return new HttpResponse(200, "OK", val); + } else if(typeof val === "object" && val.statusCode) { + // HttpResponse payload + return val as HttpResponse; } else { - return val; + // JSON payload + return new HttpResponse(200, "OK", JSON.stringify(val)); } } else { return await oldHandler(request); diff --git a/client-ts/signalr/spec/Utils.ts b/client-ts/signalr/spec/Utils.ts index 2c856715d5..4df320a066 100644 --- a/client-ts/signalr/spec/Utils.ts +++ b/client-ts/signalr/spec/Utils.ts @@ -56,4 +56,4 @@ export class PromiseSource { public reject(reason?: any) { this.rejecter(reason); } -} +} \ No newline at end of file diff --git a/client-ts/signalr/src/Base64EncodedHubProtocol.ts b/client-ts/signalr/src/Base64EncodedHubProtocol.ts deleted file mode 100644 index 9cd2d2abc5..0000000000 --- a/client-ts/signalr/src/Base64EncodedHubProtocol.ts +++ /dev/null @@ -1,60 +0,0 @@ -// 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, ProtocolType } from "./IHubProtocol"; - -export class Base64EncodedHubProtocol implements IHubProtocol { - private wrappedProtocol: IHubProtocol; - - constructor(protocol: IHubProtocol) { - this.wrappedProtocol = protocol; - this.name = this.wrappedProtocol.name; - this.type = ProtocolType.Text; - } - - public readonly name: string; - public readonly type: ProtocolType; - - public parseMessages(input: any): HubMessage[] { - // The format of the message is `size:message;` - const pos = input.indexOf(":"); - if (pos === -1 || input[input.length - 1] !== ";") { - throw new Error("Invalid payload."); - } - - const lenStr = input.substring(0, pos); - if (!/^[0-9]+$/.test(lenStr)) { - throw new Error(`Invalid length: '${lenStr}'`); - } - - const messageSize = parseInt(lenStr, 10); - // 2 accounts for ':' after message size and trailing ';' - if (messageSize !== input.length - pos - 2) { - throw new Error("Invalid message size."); - } - - const encodedMessage = input.substring(pos + 1, input.length - 1); - - // atob/btoa are browsers APIs but they can be polyfilled. If this becomes problematic we can use - // base64-js module - const s = atob(encodedMessage); - const payload = new Uint8Array(s.length); - for (let i = 0; i < payload.length; i++) { - payload[i] = s.charCodeAt(i); - } - return this.wrappedProtocol.parseMessages(payload.buffer); - } - - public writeMessage(message: HubMessage): any { - const payload = new Uint8Array(this.wrappedProtocol.writeMessage(message)); - let s = ""; - for (let i = 0; i < payload.byteLength; i++) { - s += String.fromCharCode(payload[i]); - } - // atob/btoa are browsers APIs but they can be polyfilled. If this becomes problematic we can use - // base64-js module - const encodedMessage = btoa(s); - - return `${encodedMessage.length.toString()}:${encodedMessage};`; - } -} diff --git a/client-ts/signalr/src/HttpConnection.ts b/client-ts/signalr/src/HttpConnection.ts index e2f5fd4e43..98b97f30ec 100644 --- a/client-ts/signalr/src/HttpConnection.ts +++ b/client-ts/signalr/src/HttpConnection.ts @@ -6,7 +6,8 @@ import { DefaultHttpClient, HttpClient } from "./HttpClient"; import { IConnection } from "./IConnection"; import { ILogger, LogLevel } from "./ILogger"; import { LoggerFactory } from "./Loggers"; -import { ITransport, LongPollingTransport, ServerSentEventsTransport, TransferMode, TransportType, WebSocketTransport } from "./Transports"; +import { ITransport, LongPollingTransport, ServerSentEventsTransport, TransferFormat, TransportType, WebSocketTransport } from "./Transports"; +import { Arg } from "./Utils"; export interface IHttpConnectionOptions { httpClient?: HttpClient; @@ -23,7 +24,12 @@ const enum ConnectionState { interface INegotiateResponse { connectionId: string; - availableTransports: string[]; + availableTransports: IAvailableTransport[]; +} + +interface IAvailableTransport { + transport: keyof typeof TransportType; + transferFormats: Array; } export class HttpConnection implements IConnection { @@ -33,13 +39,19 @@ export class HttpConnection implements IConnection { private readonly httpClient: HttpClient; private readonly logger: ILogger; private readonly options: IHttpConnectionOptions; + private readonly transferFormat: TransferFormat; private transport: ITransport; private connectionId: string; private startPromise: Promise; public readonly features: any = {}; - constructor(url: string, options: IHttpConnectionOptions = {}) { + constructor(url: string, transferFormat: TransferFormat, options: IHttpConnectionOptions = {}) { + Arg.isRequired(url, "url"); + Arg.isRequired(transferFormat, "transferFormat"); + Arg.isIn(transferFormat, TransferFormat, "transferFormat"); + + this.transferFormat = transferFormat; this.logger = LoggerFactory.createLogger(options.logger); this.baseUrl = this.resolveUrl(url); @@ -67,7 +79,7 @@ export class HttpConnection implements IConnection { if (this.options.transport === TransportType.WebSockets) { // No need to add a connection ID in this case this.url = this.baseUrl; - this.transport = this.createTransport(this.options.transport, [TransportType[TransportType.WebSockets]]); + this.transport = this.constructTransport(TransportType.WebSockets); } else { let headers; const token = this.options.accessTokenFactory(); @@ -91,50 +103,73 @@ export class HttpConnection implements IConnection { if (this.connectionId) { this.url = this.baseUrl + (this.baseUrl.indexOf("?") === -1 ? "?" : "&") + `id=${this.connectionId}`; - this.transport = this.createTransport(this.options.transport, negotiateResponse.availableTransports); + this.transport = this.createTransport(this.options.transport, negotiateResponse.availableTransports, this.transferFormat); } } this.transport.onreceive = this.onreceive; this.transport.onclose = (e) => this.stopConnection(true, e); - const requestedTransferMode = - this.features.transferMode === TransferMode.Binary - ? TransferMode.Binary - : TransferMode.Text; - - this.features.transferMode = await this.transport.connect(this.url, requestedTransferMode, this); + await this.transport.connect(this.url, this.transferFormat, this); // only change the state if we were connecting to not overwrite // the state if the connection is already marked as Disconnected this.changeState(ConnectionState.Connecting, ConnectionState.Connected); } catch (e) { - this.logger.log(LogLevel.Error, "Failed to start the connection. " + e); + this.logger.log(LogLevel.Error, "Failed to start the connection: " + e); this.connectionState = ConnectionState.Disconnected; this.transport = null; throw e; } } - private createTransport(transport: TransportType | ITransport, availableTransports: string[]): ITransport { - if ((transport === null || transport === undefined) && availableTransports.length > 0) { - transport = TransportType[availableTransports[0]]; - } - if (transport === TransportType.WebSockets && availableTransports.indexOf(TransportType[transport]) >= 0) { - return new WebSocketTransport(this.options.accessTokenFactory, this.logger); - } - if (transport === TransportType.ServerSentEvents && availableTransports.indexOf(TransportType[transport]) >= 0) { - return new ServerSentEventsTransport(this.httpClient, this.options.accessTokenFactory, this.logger); - } - if (transport === TransportType.LongPolling && availableTransports.indexOf(TransportType[transport]) >= 0) { - return new LongPollingTransport(this.httpClient, this.options.accessTokenFactory, this.logger); + private createTransport(requestedTransport: TransportType | ITransport, availableTransports: IAvailableTransport[], requestedTransferFormat: TransferFormat): ITransport { + if (this.isITransport(requestedTransport)) { + this.logger.log(LogLevel.Trace, "Connection was provided an instance of ITransport, using that directly."); + return requestedTransport; } - if (this.isITransport(transport)) { - return transport; + for (const endpoint of availableTransports) { + const transport = this.resolveTransport(endpoint, requestedTransport, requestedTransferFormat); + if (transport) { + return this.constructTransport(transport); + } } - throw new Error("No available transports found."); + throw new Error("Unable to initialize any of the available transports."); + } + + private constructTransport(transport: TransportType) { + switch (transport) { + case TransportType.WebSockets: + return new WebSocketTransport(this.options.accessTokenFactory, this.logger); + case TransportType.ServerSentEvents: + return new ServerSentEventsTransport(this.httpClient, this.options.accessTokenFactory, this.logger); + case TransportType.LongPolling: + return new LongPollingTransport(this.httpClient, this.options.accessTokenFactory, this.logger); + default: + throw new Error(`Unknown transport: ${transport}.`); + } + } + + private resolveTransport(endpoint: IAvailableTransport, requestedTransport: TransportType, requestedTransferFormat: TransferFormat): TransportType | null { + const transport = TransportType[endpoint.transport]; + if (!transport) { + this.logger.log(LogLevel.Trace, `Skipping transport '${endpoint.transport}' because it is not supported by this client.`); + } else { + const transferFormats = endpoint.transferFormats.map((s) => TransferFormat[s]); + if (!requestedTransport || transport === requestedTransport) { + if (transferFormats.indexOf(requestedTransferFormat) >= 0) { + this.logger.log(LogLevel.Trace, `Selecting transport '${transport}'`); + return transport; + } else { + this.logger.log(LogLevel.Trace, `Skipping transport '${transport}' because it does not support the requested transfer format '${requestedTransferFormat}'.`); + } + } else { + this.logger.log(LogLevel.Trace, `Skipping transport '${transport}' because it was disabled by the client.`); + } + } + return null; } private isITransport(transport: any): transport is ITransport { @@ -151,7 +186,7 @@ export class HttpConnection implements IConnection { public send(data: any): Promise { if (this.connectionState !== ConnectionState.Connected) { - throw new Error("Cannot send data if the connection is not in the 'Connected' State"); + throw new Error("Cannot send data if the connection is not in the 'Connected' State."); } return this.transport.send(data); @@ -210,7 +245,7 @@ export class HttpConnection implements IConnection { } const normalizedUrl = baseUrl + url; - this.logger.log(LogLevel.Information, `Normalizing '${url}' to '${normalizedUrl}'`); + this.logger.log(LogLevel.Information, `Normalizing '${url}' to '${normalizedUrl}'.`); return normalizedUrl; } diff --git a/client-ts/signalr/src/HubConnection.ts b/client-ts/signalr/src/HubConnection.ts index 16accc998a..ccdb31fabd 100644 --- a/client-ts/signalr/src/HubConnection.ts +++ b/client-ts/signalr/src/HubConnection.ts @@ -1,17 +1,16 @@ // 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 { Base64EncodedHubProtocol } from "./Base64EncodedHubProtocol"; import { ConnectionClosed } from "./Common"; import { HttpConnection, IHttpConnectionOptions } from "./HttpConnection"; import { IConnection } from "./IConnection"; -import { CancelInvocationMessage, CompletionMessage, HubMessage, IHubProtocol, InvocationMessage, MessageType, NegotiationMessage, ProtocolType, StreamInvocationMessage, StreamItemMessage } from "./IHubProtocol"; +import { CancelInvocationMessage, CompletionMessage, HubMessage, IHubProtocol, InvocationMessage, MessageType, NegotiationMessage, StreamInvocationMessage, StreamItemMessage } from "./IHubProtocol"; import { ILogger, LogLevel } from "./ILogger"; import { JsonHubProtocol } from "./JsonHubProtocol"; import { ConsoleLogger, LoggerFactory, NullLogger } from "./Loggers"; import { Observable, Subject } from "./Observable"; import { TextMessageFormat } from "./TextMessageFormat"; -import { TransferMode, TransportType } from "./Transports"; +import { TransferFormat, TransportType } from "./Transports"; export { JsonHubProtocol }; @@ -40,15 +39,16 @@ export class HubConnection { this.timeoutInMilliseconds = options.timeoutInMilliseconds || DEFAULT_TIMEOUT_IN_MS; + this.protocol = options.protocol || new JsonHubProtocol(); + if (typeof urlOrConnection === "string") { - this.connection = new HttpConnection(urlOrConnection, options); + this.connection = new HttpConnection(urlOrConnection, this.protocol.transferFormat, options); } else { this.connection = urlOrConnection; } this.logger = LoggerFactory.createLogger(options.logger); - this.protocol = options.protocol || new JsonHubProtocol(); this.connection.onreceive = (data: any) => this.processIncomingData(data); this.connection.onclose = (error?: Error) => this.connectionClosed(error); @@ -133,14 +133,7 @@ export class HubConnection { } public async start(): Promise { - const requestedTransferMode = - (this.protocol.type === ProtocolType.Binary) - ? TransferMode.Binary - : TransferMode.Text; - - this.connection.features.transferMode = requestedTransferMode; await this.connection.start(); - const actualTransferMode = this.connection.features.transferMode; await this.connection.send( TextMessageFormat.write( @@ -148,10 +141,6 @@ export class HubConnection { this.logger.log(LogLevel.Information, `Using HubProtocol '${this.protocol.name}'.`); - if (requestedTransferMode === TransferMode.Binary && actualTransferMode === TransferMode.Text) { - this.protocol = new Base64EncodedHubProtocol(this.protocol); - } - this.configureTimeout(); } diff --git a/client-ts/signalr/src/IConnection.ts b/client-ts/signalr/src/IConnection.ts index f4e62c8c07..0e8c347996 100644 --- a/client-ts/signalr/src/IConnection.ts +++ b/client-ts/signalr/src/IConnection.ts @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. import { ConnectionClosed, DataReceived } from "./Common"; -import { ITransport, TransferMode, TransportType } from "./Transports"; +import { ITransport, TransportType } from "./Transports"; export interface IConnection { readonly features: any; diff --git a/client-ts/signalr/src/IHubProtocol.ts b/client-ts/signalr/src/IHubProtocol.ts index e65e171867..02e9aa8733 100644 --- a/client-ts/signalr/src/IHubProtocol.ts +++ b/client-ts/signalr/src/IHubProtocol.ts @@ -1,4 +1,6 @@ -// Copyright (c) .NET Foundation. All rights reserved. +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. export const enum MessageType { @@ -58,14 +60,9 @@ export interface CancelInvocationMessage extends HubInvocationMessage { readonly type: MessageType.CancelInvocation; } -export const enum ProtocolType { - Text = 1, - Binary, -} - export interface IHubProtocol { readonly name: string; - readonly type: ProtocolType; + readonly transferFormat: TransferFormat; parseMessages(input: any): HubMessage[]; writeMessage(message: HubMessage): any; } diff --git a/client-ts/signalr/src/JsonHubProtocol.ts b/client-ts/signalr/src/JsonHubProtocol.ts index c2119455c0..da6dd67736 100644 --- a/client-ts/signalr/src/JsonHubProtocol.ts +++ b/client-ts/signalr/src/JsonHubProtocol.ts @@ -1,8 +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, ProtocolType } from "./IHubProtocol"; +import { HubMessage, IHubProtocol } from "./IHubProtocol"; import { TextMessageFormat } from "./TextMessageFormat"; +import { TransferFormat } from "./Transports"; export const JSON_HUB_PROTOCOL_NAME: string = "json"; @@ -10,7 +11,7 @@ export class JsonHubProtocol implements IHubProtocol { public readonly name: string = JSON_HUB_PROTOCOL_NAME; - public readonly type: ProtocolType = ProtocolType.Text; + public readonly transferFormat: TransferFormat = TransferFormat.Text; public parseMessages(input: string): HubMessage[] { if (!input) { diff --git a/client-ts/signalr/src/Transports.ts b/client-ts/signalr/src/Transports.ts index 9ce1f90c24..fece7aa26b 100644 --- a/client-ts/signalr/src/Transports.ts +++ b/client-ts/signalr/src/Transports.ts @@ -7,6 +7,7 @@ import { HttpError, TimeoutError } from "./Errors"; import { HttpClient, HttpRequest } from "./HttpClient"; import { IConnection } from "./IConnection"; import { ILogger, LogLevel } from "./ILogger"; +import { Arg } from "./Utils"; export enum TransportType { WebSockets, @@ -14,13 +15,13 @@ export enum TransportType { LongPolling, } -export const enum TransferMode { +export enum TransferFormat { Text = 1, Binary, } export interface ITransport { - connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise; + connect(url: string, transferFormat: TransferFormat, connection: IConnection): Promise; send(data: any): Promise; stop(): Promise; onreceive: DataReceived; @@ -37,9 +38,17 @@ export class WebSocketTransport implements ITransport { this.accessTokenFactory = accessTokenFactory || (() => null); } - public connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise { + public connect(url: string, transferFormat: TransferFormat, connection: IConnection): Promise { + Arg.isRequired(url, "url"); + Arg.isRequired(transferFormat, "transferFormat"); + Arg.isIn(transferFormat, TransferFormat, "transferFormat"); + Arg.isRequired(connection, "connection"); - return new Promise((resolve, reject) => { + if (typeof (WebSocket) === "undefined") { + throw new Error("'WebSocket' is not supported in your environment."); + } + + return new Promise((resolve, reject) => { url = url.replace(/^http/, "ws"); const token = this.accessTokenFactory(); if (token) { @@ -47,18 +56,18 @@ export class WebSocketTransport implements ITransport { } const webSocket = new WebSocket(url); - if (requestedTransferMode === TransferMode.Binary) { + if (transferFormat === TransferFormat.Binary) { webSocket.binaryType = "arraybuffer"; } webSocket.onopen = (event: Event) => { this.logger.log(LogLevel.Information, `WebSocket connected to ${url}`); this.webSocket = webSocket; - resolve(requestedTransferMode); + resolve(); }; - webSocket.onerror = (event: Event) => { - reject(); + webSocket.onerror = (event: ErrorEvent) => { + reject(event.error); }; webSocket.onmessage = (message: MessageEvent) => { @@ -115,13 +124,22 @@ export class ServerSentEventsTransport implements ITransport { this.logger = logger; } - public connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise { + public connect(url: string, transferFormat: TransferFormat, connection: IConnection): Promise { + Arg.isRequired(url, "url"); + Arg.isRequired(transferFormat, "transferFormat"); + Arg.isIn(transferFormat, TransferFormat, "transferFormat"); + Arg.isRequired(connection, "connection"); + if (typeof (EventSource) === "undefined") { - Promise.reject("EventSource not supported by the browser."); + throw new Error("'EventSource' is not supported in your environment."); } this.url = url; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { + if (transferFormat !== TransferFormat.Text) { + reject(new Error("The Server-Sent Events transport only supports the 'Text' transfer format")); + } + const token = this.accessTokenFactory(); if (token) { url += (url.indexOf("?") < 0 ? "?" : "&") + `access_token=${encodeURIComponent(token)}`; @@ -145,7 +163,7 @@ export class ServerSentEventsTransport implements ITransport { }; eventSource.onerror = (e: any) => { - reject(); + reject(new Error(e.message || "Error occurred")); // don't report an error if the transport did not start successfully if (this.eventSource && this.onclose) { @@ -157,7 +175,7 @@ export class ServerSentEventsTransport implements ITransport { this.logger.log(LogLevel.Information, `SSE connected to ${this.url}`); this.eventSource = eventSource; // SSE is a text protocol - resolve(TransferMode.Text); + resolve(); }; } catch (e) { return Promise.reject(e); @@ -197,29 +215,34 @@ export class LongPollingTransport implements ITransport { this.pollAbort = new AbortController(); } - public connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise { + public connect(url: string, transferFormat: TransferFormat, connection: IConnection): Promise { + Arg.isRequired(url, "url"); + Arg.isRequired(transferFormat, "transferFormat"); + Arg.isIn(transferFormat, TransferFormat, "transferFormat"); + Arg.isRequired(connection, "connection"); + this.url = url; // Set a flag indicating we have inherent keep-alive in this transport. connection.features.inherentKeepAlive = true; - if (requestedTransferMode === TransferMode.Binary && (typeof new XMLHttpRequest().responseType !== "string")) { + if (transferFormat === TransferFormat.Binary && (typeof new XMLHttpRequest().responseType !== "string")) { // This will work if we fix: https://github.com/aspnet/SignalR/issues/742 throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported."); } - this.poll(this.url, requestedTransferMode); - return Promise.resolve(requestedTransferMode); + this.poll(this.url, transferFormat); + return Promise.resolve(); } - private async poll(url: string, transferMode: TransferMode): Promise { + private async poll(url: string, transferFormat: TransferFormat): Promise { const pollOptions: HttpRequest = { abortSignal: this.pollAbort.signal, headers: new Map(), timeout: 90000, }; - if (transferMode === TransferMode.Binary) { + if (transferFormat === TransferFormat.Binary) { pollOptions.responseType = "arraybuffer"; } diff --git a/client-ts/signalr/src/Utils.ts b/client-ts/signalr/src/Utils.ts new file mode 100644 index 0000000000..72d0ad875a --- /dev/null +++ b/client-ts/signalr/src/Utils.ts @@ -0,0 +1,17 @@ +// 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. + +export class Arg { + public static isRequired(val: any, name: string): void { + if (val === null || val === undefined) { + throw new Error(`The '${name}' argument is required.`); + } + } + + public static isIn(val: any, values: any, name: string): void { + // TypeScript enums have keys for **both** the name and the value of each enum member on the type itself. + if (!(val in values)) { + throw new Error(`Unknown ${name} value: ${val}.`); + } + } +} diff --git a/samples/ClientSample/RawSample.cs b/samples/ClientSample/RawSample.cs index e2e047c1de..571cfe1ec3 100644 --- a/samples/ClientSample/RawSample.cs +++ b/samples/ClientSample/RawSample.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -8,6 +8,7 @@ using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets.Client; using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.Logging; @@ -42,7 +43,7 @@ namespace ClientSample var closeTcs = new TaskCompletionSource(); connection.Closed += e => closeTcs.SetResult(null); connection.OnReceived(data => Console.Out.WriteLineAsync($"{Encoding.UTF8.GetString(data)}")); - await connection.StartAsync(); + await connection.StartAsync(TransferFormat.Text); Console.WriteLine($"Connected to {baseUrl}"); var cts = new CancellationTokenSource(); diff --git a/samples/SocialWeather/PersistentConnectionLifeTimeManager.cs b/samples/SocialWeather/PersistentConnectionLifeTimeManager.cs index 577254ae15..ef77fa9b4c 100644 --- a/samples/SocialWeather/PersistentConnectionLifeTimeManager.cs +++ b/samples/SocialWeather/PersistentConnectionLifeTimeManager.cs @@ -29,10 +29,10 @@ namespace SocialWeather connection.Features.Get().Metadata["format"] = format; if (string.Equals(format, "protobuf", StringComparison.OrdinalIgnoreCase)) { - var transferModeFeature = connection.Features.Get(); - if (transferModeFeature != null) + var transferFormatFeature = connection.Features.Get(); + if (transferFormatFeature != null) { - transferModeFeature.TransferMode = TransferMode.Binary; + transferFormatFeature.ActiveFormat = TransferFormat.Binary; } } _connectionList.Add(connection); diff --git a/samples/SocketsSample/Startup.cs b/samples/SocketsSample/Startup.cs index e15502d52d..76eb958f83 100644 --- a/samples/SocketsSample/Startup.cs +++ b/samples/SocketsSample/Startup.cs @@ -4,6 +4,7 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Sockets; using Microsoft.Extensions.DependencyInjection; using MsgPack.Serialization; using SocketsSample.EndPoints; diff --git a/samples/SocketsSample/web.config b/samples/SocketsSample/web.config index dc0514fca5..8700b60c05 100644 --- a/samples/SocketsSample/web.config +++ b/samples/SocketsSample/web.config @@ -1,14 +1,12 @@  - - - + - + - + \ No newline at end of file diff --git a/samples/SocketsSample/wwwroot/hubs.html b/samples/SocketsSample/wwwroot/hubs.html index 99002d3f48..8e37d603e1 100644 --- a/samples/SocketsSample/wwwroot/hubs.html +++ b/samples/SocketsSample/wwwroot/hubs.html @@ -15,7 +15,8 @@ @@ -135,14 +136,19 @@ var connection; click('connect', function (event) { - let transportType = signalR.TransportType[transportDropdown.value] || signalR.TransportType.WebSockets; let hubRoute = hubTypeDropdown.value || "default"; let protocol = protocolDropdown.value === "msgpack" ? new signalR.protocols.msgpack.MessagePackHubProtocol() : new signalR.JsonHubProtocol(); + let options = { logger: logger, protocol: protocol }; + + if (transportDropdown.value !== "Automatic") { + options.transport = signalR.TransportType[transportDropdown.value]; + } + console.log('http://' + document.location.host + '/' + hubRoute); - connection = new signalR.HubConnection(hubRoute, { transport: transportType, logger: logger, protocol: protocol }); + connection = new signalR.HubConnection(hubRoute, options); connection.on('Send', function (msg) { addLine('message-list', msg); }); diff --git a/specs/TransportProtocols.md b/specs/TransportProtocols.md index b274c905ae..db7ffbec2b 100644 --- a/specs/TransportProtocols.md +++ b/specs/TransportProtocols.md @@ -23,10 +23,34 @@ The `POST [endpoint-base]/negotiate` request is used to establish connection bet ``` { "connectionId":"807809a5-31bf-470d-9e23-afaee35d8a0d", - "availableTransports":["WebSockets","ServerSentEvents","LongPolling"] + "availableTransports":[ + { + "transport": "WebSockets", + "transferFormats": [ "Text", "Binary" ] + }, + { + "transport": "ServerSentEvents", + "transferFormats": [ "Text" ] + }, + { + "transport": "LongPolling", + "transferFormats": [ "Text", "Binary" ] + } + ] } ``` +The payload returned from this endpoint provides the following data: + +* The `connectionId` which is **required** by the Long Polling and Server-Sent Events transports (in order to correlate sends and receives). +* The `availableTransports` list which describes the transports the server supports. For each transport, the name of the transport (`transport`) is listed, as is a list of "transfer formats" supported by the transport (`transferFormats`) + +## Transfer Formats + +ASP.NET Endpoints support two different transfer formats: `Text` and `Binary`. `Text` refers to UTF-8 text, and `Binary` refers to any arbitrary binary data. The transfer format serves two purposes. First, in the WebSockets transport, it is used to determine if `Text` or `Binary` WebSocket frames should be used to carry data. This is useful in debugging as most browser Dev Tools only show the content of `Text` frames. When using a text-based protocol like JSON, it is preferable for the WebSockets transport to use `Text` frames. How a client/server indicate the transfer format currently being used is implementation-defined. + +Some transports are limited to supporting only `Text` data (specifically, Server-Sent Events). These transports cannot carry arbitrary binary data (without additional encoding, such as Base-64) due to limitations in their protocol. The transfer formats supported by each transport are described as part of the `POST [endpoint-base]/negotiate` response to allow clients to ignore transports that cannot support arbitrary binary data when they have a need to send/receive that data. How the client indicates the transfer format it wishes to use is also implementation-defined. + ## WebSockets (Full Duplex) The WebSockets transport is unique in that it is full duplex, and a persistent connection that can be established in a single operation. As a result, the client is not required to use the `POST [endpoint-base]/negotiate` request to establish a connection in advance. It also includes all the necessary metadata in it's own frame metadata. @@ -53,7 +77,6 @@ If the relevant connection has been terminated, a `404 Not Found` status code is Server-Sent Events (SSE) is a protocol specified by WHATWG at [https://html.spec.whatwg.org/multipage/comms.html#server-sent-events](https://html.spec.whatwg.org/multipage/comms.html#server-sent-events). It is capable of sending data from server to client only, so it must be paired with the HTTP Post transport. It also requires a connection already be established using the `POST [endpoint-base]/negotiate` request. - The protocol is similar to Long Polling in that the client opens a request to an endpoint and leaves it open. The server transmits frames as "events" using the SSE protocol. The protocol encodes a single event as a sequence of key-value pair lines, separated by `:` and using any of `\r\n`, `\n` or `\r` as line-terminators, followed by a final blank line. Keys can be duplicated and their values are concatenated with `\n`. So the following represents two events: ``` @@ -71,6 +94,8 @@ In the first event, the value of `baz` would be `boz\nbiz\nflarg`, due to the co In this transport, the client establishes an SSE connection to `[endpoint-base]` with an `Accept` header of `text/event-stream`, and the server responds with an HTTP response with a `Content-Type` of `text/event-stream`. The **mandatory** `connectionId` query string value is used to identify the connection to send to. If there is no `connectionId` query string value, a `400 Bad Request` response is returned, if there is no connection with the specified ID, a `404 Not Found` response is returned. Each SSE event represents a single frame from client to server. The transport uses unnamed events, which means only the `data` field is available. Thus we use the first line of the `data` field for frame metadata. +The Server-Sent Events transport only supports text data, because it is a text-based protocol. As a result, it is reported by the server as supporting only the `Text` transfer format. If a client wishes to send arbitrary binary data, it should skip the Server-Sent Events transport when selecting an appropriate transport. + ## Long Polling (Server-to-Client only) Long Polling is a server-to-client half-transport, so it is always paired with HTTP Post. It requires a connection already be established using the `POST [endpoint-base]/negotiate` request. diff --git a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs index 535989cfe6..ab6ad99ea0 100644 --- a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs +++ b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs @@ -1,4 +1,4 @@ -// 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. using System; diff --git a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs index ff4183ef4a..238070dd69 100644 --- a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs +++ b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs @@ -4,13 +4,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Threading; -using System.Threading.Tasks; using System.Threading.Channels; +using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Internal; -using Microsoft.AspNetCore.SignalR.Internal.Encoders; using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets.Client; @@ -30,7 +28,6 @@ namespace Microsoft.AspNetCore.SignalR.Client private readonly IConnection _connection; private readonly IHubProtocol _protocol; private readonly HubBinder _binder; - private HubProtocolReaderWriter _protocolReaderWriter; private readonly object _pendingCallsLock = new object(); private readonly Dictionary _pendingCalls = new Dictionary(); @@ -113,26 +110,9 @@ namespace Microsoft.AspNetCore.SignalR.Client private async Task StartAsyncCore() { - var transferModeFeature = _connection.Features.Get(); - if (transferModeFeature == null) - { - transferModeFeature = new TransferModeFeature(); - _connection.Features.Set(transferModeFeature); - } - - var requestedTransferMode = - _protocol.Type == ProtocolType.Binary - ? TransferMode.Binary - : TransferMode.Text; - - transferModeFeature.TransferMode = requestedTransferMode; - await _connection.StartAsync(); + await _connection.StartAsync(_protocol.TransferFormat); _needKeepAlive = _connection.Features.Get() == null; - var actualTransferMode = transferModeFeature.TransferMode; - - _protocolReaderWriter = new HubProtocolReaderWriter(_protocol, GetDataEncoder(requestedTransferMode, actualTransferMode)); - Log.HubProtocol(_logger, _protocol.Name); _connectionActive = new CancellationTokenSource(); @@ -146,20 +126,6 @@ namespace Microsoft.AspNetCore.SignalR.Client ResetTimeoutTimer(); } - private IDataEncoder GetDataEncoder(TransferMode requestedTransferMode, TransferMode actualTransferMode) - { - if (requestedTransferMode == TransferMode.Binary && actualTransferMode == TransferMode.Text) - { - // This is for instance for SSE which is a Text protocol and the user wants to use a binary - // protocol so we need to encode messages. - return new Base64Encoder(); - } - - Debug.Assert(requestedTransferMode == actualTransferMode, "All transports besides SSE are expected to support binary mode."); - - return new PassThroughEncoder(); - } - public async Task StopAsync() => await StopAsyncCore().ForceAsync(); private Task StopAsyncCore() => _connection.StopAsync(); @@ -295,7 +261,7 @@ namespace Microsoft.AspNetCore.SignalR.Client { try { - var payload = _protocolReaderWriter.WriteMessage(hubMessage); + var payload = _protocol.WriteToArray(hubMessage); Log.SendInvocation(_logger, hubMessage.InvocationId); await _connection.SendAsync(payload, irq.CancellationToken); @@ -328,7 +294,7 @@ namespace Microsoft.AspNetCore.SignalR.Client { Log.PreparingNonBlockingInvocation(_logger, methodName, args.Length); - var payload = _protocolReaderWriter.WriteMessage(invocationMessage); + var payload = _protocol.WriteToArray(invocationMessage); Log.SendInvocation(_logger, invocationMessage.InvocationId); await _connection.SendAsync(payload, cancellationToken); @@ -345,7 +311,8 @@ namespace Microsoft.AspNetCore.SignalR.Client { ResetTimeoutTimer(); Log.ParsingMessages(_logger, data.Length); - if (_protocolReaderWriter.ReadMessages(data, _binder, out var messages)) + var messages = new List(); + if (_protocol.TryParseMessages(data, _binder, messages)) { Log.ReceivingMessages(_logger, messages.Count); foreach (var message in messages) @@ -621,10 +588,5 @@ namespace Microsoft.AspNetCore.SignalR.Client return _callback(parameters, _state); } } - - private class TransferModeFeature : ITransferModeFeature - { - public TransferMode TransferMode { get; set; } - } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/Base64Encoder.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/Base64Encoder.cs deleted file mode 100644 index 050f746d0a..0000000000 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/Base64Encoder.cs +++ /dev/null @@ -1,59 +0,0 @@ -// 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. - -using System; -using System.Buffers; -using System.Buffers.Text; -using System.Diagnostics; - -namespace Microsoft.AspNetCore.SignalR.Internal.Encoders -{ - public class Base64Encoder : IDataEncoder - { - public bool TryDecode(ref ReadOnlySpan buffer, out ReadOnlySpan data) - { - if (LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out var message)) - { - Span decoded = new byte[Base64.GetMaxDecodedFromUtf8Length(message.Length)]; - var status = Base64.DecodeFromUtf8(message, decoded, out _, out var written); - Debug.Assert(status == OperationStatus.Done); - data = decoded.Slice(0, written); - return true; - } - return false; - } - - private const int Int32OverflowLength = 10; - - public byte[] Encode(byte[] payload) - { - var maxEncodedLength = Base64.GetMaxEncodedToUtf8Length(payload.Length); - - // Int32OverflowLength + length of separator (':') + length of terminator (';') - if (int.MaxValue - maxEncodedLength < Int32OverflowLength + 2) - { - throw new FormatException("The encoded message exceeds the maximum supported size."); - } - - //The format is: [{length}:{message};] so allocate enough to be able to write the entire message - Span buffer = new byte[Int32OverflowLength + 1 + maxEncodedLength + 1]; - - buffer[Int32OverflowLength] = (byte)':'; - var status = Base64.EncodeToUtf8(payload, buffer.Slice(Int32OverflowLength + 1), out _, out var written); - Debug.Assert(status == OperationStatus.Done); - - buffer[Int32OverflowLength + 1 + written] = (byte)';'; - var prefixLength = 0; - var prefix = written; - do - { - buffer[Int32OverflowLength - 1 - prefixLength] = (byte)('0' + prefix % 10); - prefix /= 10; - prefixLength++; - } - while (prefix > 0); - - return buffer.Slice(Int32OverflowLength - prefixLength, prefixLength + 1 + written + 1).ToArray(); - } - } -} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/IDataEncoder.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/IDataEncoder.cs deleted file mode 100644 index f7f7146076..0000000000 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/IDataEncoder.cs +++ /dev/null @@ -1,13 +0,0 @@ -// 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. - -using System; - -namespace Microsoft.AspNetCore.SignalR.Internal.Encoders -{ - public interface IDataEncoder - { - byte[] Encode(byte[] payload); - bool TryDecode(ref ReadOnlySpan buffer, out ReadOnlySpan data); - } -} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/LengthPrefixedTextMessageParser.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/LengthPrefixedTextMessageParser.cs deleted file mode 100644 index 6fb57c1288..0000000000 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/LengthPrefixedTextMessageParser.cs +++ /dev/null @@ -1,94 +0,0 @@ -// 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. - -using System; -using System.Buffers.Text; -using System.Text; - -namespace Microsoft.AspNetCore.SignalR.Internal.Encoders -{ - public static class LengthPrefixedTextMessageParser - { - private const char FieldDelimiter = ':'; - private const char MessageDelimiter = ';'; - - /// - /// Attempts to parse a message from the buffer. Returns 'false' if there is not enough data to complete a message. Throws an - /// exception if there is a format error in the provided data. - /// - public static bool TryParseMessage(ref ReadOnlySpan buffer, out ReadOnlySpan payload) - { - payload = default; - - if (!TryReadLength(buffer, out var index, out var length)) - { - return false; - } - - var remaining = buffer.Slice(index); - - if (!TryReadDelimiter(remaining, FieldDelimiter, "length")) - { - return false; - } - - // Skip the delimeter - remaining = remaining.Slice(1); - - if (remaining.Length < length + 1) - { - return false; - } - - payload = remaining.Slice(0, length); - - remaining = remaining.Slice(length); - - if (!TryReadDelimiter(remaining, MessageDelimiter, "payload")) - { - return false; - } - - // Skip the delimeter - buffer = remaining.Slice(1); - return true; - } - - private static bool TryReadLength(ReadOnlySpan buffer, out int index, out int length) - { - length = 0; - // Read until the first ':' to find the length - index = buffer.IndexOf((byte)FieldDelimiter); - - if (index == -1) - { - // Insufficient data - return false; - } - - var lengthSpan = buffer.Slice(0, index); - - if (!Utf8Parser.TryParse(buffer, out length, out var bytesConsumed) || bytesConsumed < lengthSpan.Length) - { - throw new FormatException($"Invalid length: '{Encoding.UTF8.GetString(lengthSpan.ToArray())}'"); - } - - return true; - } - - private static bool TryReadDelimiter(ReadOnlySpan buffer, char delimiter, string field) - { - if (buffer.Length == 0) - { - return false; - } - - if (buffer[0] != delimiter) - { - throw new FormatException($"Missing delimiter '{delimiter}' after {field}"); - } - - return true; - } - } -} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/PassThroughEncoder.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/PassThroughEncoder.cs deleted file mode 100644 index e66970f9cf..0000000000 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Encoders/PassThroughEncoder.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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. - -using System; - -namespace Microsoft.AspNetCore.SignalR.Internal.Encoders -{ - public class PassThroughEncoder : IDataEncoder - { - public bool TryDecode(ref ReadOnlySpan buffer, out ReadOnlySpan data) - { - data = buffer; - buffer = Array.Empty(); - return true; - } - - public byte[] Encode(byte[] payload) - { - return payload; - } - } -} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/HubProtocolReaderWriter.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/HubProtocolReaderWriter.cs deleted file mode 100644 index f2ea899282..0000000000 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/HubProtocolReaderWriter.cs +++ /dev/null @@ -1,74 +0,0 @@ -// 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. - -using System; -using System.Buffers; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using Microsoft.AspNetCore.SignalR.Internal.Encoders; -using Microsoft.AspNetCore.SignalR.Internal.Protocol; - -namespace Microsoft.AspNetCore.SignalR.Internal -{ - public class HubProtocolReaderWriter - { - private readonly IHubProtocol _hubProtocol; - private readonly IDataEncoder _dataEncoder; - - public HubProtocolReaderWriter(IHubProtocol hubProtocol, IDataEncoder dataEncoder) - { - _hubProtocol = hubProtocol; - _dataEncoder = dataEncoder; - } - - public bool ReadMessages(ReadOnlySequence buffer, IInvocationBinder binder, out IList messages, out SequencePosition consumed, out SequencePosition examined) - { - // TODO: Fix this implementation to be incremental - consumed = buffer.End; - examined = consumed; - - return ReadMessages(buffer.ToArray(), binder, out messages); - } - - public bool ReadMessages(byte[] input, IInvocationBinder binder, out IList messages) - { - messages = new List(); - ReadOnlySpan span = input; - while (span.Length > 0 && _dataEncoder.TryDecode(ref span, out var data)) - { - _hubProtocol.TryParseMessages(data, binder, messages); - } - return messages.Count > 0; - } - - public byte[] WriteMessage(HubMessage hubMessage) - { - using (var ms = new MemoryStream()) - { - _hubProtocol.WriteMessage(hubMessage, ms); - return _dataEncoder.Encode(ms.ToArray()); - } - } - - public override bool Equals(object obj) - { - var readerWriter = obj as HubProtocolReaderWriter; - if (readerWriter == null) - { - return false; - } - - // Note: ReferenceEquals on HubProtocol works for our implementation of IHubProtocolResolver because we use Singletons from DI - // However if someone replaces the implementation and returns a new ProtocolResolver for every connection they wont get the perf benefits - // Memory growth is mitigated by capping the cache size - return ReferenceEquals(_dataEncoder, readerWriter._dataEncoder) && ReferenceEquals(_hubProtocol, readerWriter._hubProtocol); - } - - // This should never be used, needed because you can't override Equals without it - public override int GetHashCode() - { - return base.GetHashCode(); - } - } -} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMessage.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMessage.cs index 196ec7d44e..14ba3ea78d 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMessage.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubMessage.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using System.IO; namespace Microsoft.AspNetCore.SignalR.Internal.Protocol { @@ -11,39 +12,48 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol { } - // Initialize with capacity 4 for the 2 built in protocols and 2 data encoders - private readonly List _serializedMessages = new List(4); + // Initialize with capacity 2 for the 2 built in protocols + private object _lock = new object(); + private readonly List _serializedMessages = new List(2); - public byte[] WriteMessage(HubProtocolReaderWriter protocolReaderWriter) + public byte[] WriteMessage(IHubProtocol protocol) { - for (var i = 0; i < _serializedMessages.Count; i++) + // REVIEW: Revisit lock + // Could use a reader/writer lock to allow the loop to take place in "unlocked" code + // Or, could use a fixed size array and Interlocked to manage it. + // Or, Immutable *ducks* + + lock (_lock) { - if (_serializedMessages[i].ProtocolReaderWriter.Equals(protocolReaderWriter)) + for (var i = 0; i < _serializedMessages.Count; i++) { - return _serializedMessages[i].Message; + if (_serializedMessages[i].Protocol.Equals(protocol)) + { + return _serializedMessages[i].Message; + } } + + var bytes = protocol.WriteToArray(this); + + // We don't want to balloon memory if someone writes a poor IHubProtocolResolver + // So we cap how many caches we store and worst case just serialize the message for every connection + if (_serializedMessages.Count < 10) + { + _serializedMessages.Add(new SerializedMessage(protocol, bytes)); + } + + return bytes; } - - var bytes = protocolReaderWriter.WriteMessage(this); - - // We don't want to balloon memory if someone writes a poor IHubProtocolResolver - // So we cap how many caches we store and worst case just serialize the message for every connection - if (_serializedMessages.Count < 10) - { - _serializedMessages.Add(new SerializedMessage(protocolReaderWriter, bytes)); - } - - return bytes; } private readonly struct SerializedMessage { - public readonly HubProtocolReaderWriter ProtocolReaderWriter; + public readonly IHubProtocol Protocol; public readonly byte[] Message; - public SerializedMessage(HubProtocolReaderWriter protocolReaderWriter, byte[] message) + public SerializedMessage(IHubProtocol protocol, byte[] message) { - ProtocolReaderWriter = protocolReaderWriter; + Protocol = protocol; Message = message; } } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubProtocolExtensions.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubProtocolExtensions.cs new file mode 100644 index 0000000000..7710af3a80 --- /dev/null +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HubProtocolExtensions.cs @@ -0,0 +1,19 @@ +// 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. + +using System.IO; + +namespace Microsoft.AspNetCore.SignalR.Internal.Protocol +{ + public static class HubProtocolExtensions + { + public static byte[] WriteToArray(this IHubProtocol hubProtocol, HubMessage message) + { + using (var ms = new MemoryStream()) + { + hubProtocol.WriteMessage(message, ms); + return ms.ToArray(); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/IHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/IHubProtocol.cs index f00c76247f..8456f368f8 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/IHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/IHubProtocol.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using Microsoft.AspNetCore.Sockets; namespace Microsoft.AspNetCore.SignalR.Internal.Protocol { @@ -11,7 +12,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol { string Name { get; } - ProtocolType Type { get; } + TransferFormat TransferFormat { get; } bool TryParseMessages(ReadOnlySpan input, IInvocationBinder binder, IList messages); diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs index 1acd8ea84f..7f86de800d 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs @@ -7,6 +7,7 @@ using System.IO; using System.Runtime.ExceptionServices; using System.Text; using Microsoft.AspNetCore.SignalR.Internal.Formatters; +using Microsoft.AspNetCore.Sockets; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -44,7 +45,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol public string Name => ProtocolName; - public ProtocolType Type => ProtocolType.Text; + public TransferFormat TransferFormat => TransferFormat.Text; public bool TryParseMessages(ReadOnlySpan input, IInvocationBinder binder, IList messages) { diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/ProtocolType.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/ProtocolType.cs deleted file mode 100644 index 6b97082e2e..0000000000 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/ProtocolType.cs +++ /dev/null @@ -1,11 +0,0 @@ -// 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. - -namespace Microsoft.AspNetCore.SignalR.Internal.Protocol -{ - public enum ProtocolType - { - Binary, - Text - } -} diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Microsoft.AspNetCore.SignalR.Common.csproj b/src/Microsoft.AspNetCore.SignalR.Common/Microsoft.AspNetCore.SignalR.Common.csproj index 5a33158164..4bcebd9980 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Microsoft.AspNetCore.SignalR.Common.csproj +++ b/src/Microsoft.AspNetCore.SignalR.Common/Microsoft.AspNetCore.SignalR.Common.csproj @@ -15,4 +15,8 @@ + + + + diff --git a/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs b/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs index 583fcba410..25d5689f18 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs @@ -16,9 +16,7 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Protocols; using Microsoft.AspNetCore.SignalR.Core; using Microsoft.AspNetCore.SignalR.Internal; -using Microsoft.AspNetCore.SignalR.Internal.Encoders; using Microsoft.AspNetCore.SignalR.Internal.Protocol; -using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets.Features; using Microsoft.Extensions.Logging; @@ -27,8 +25,6 @@ namespace Microsoft.AspNetCore.SignalR public class HubConnectionContext { private static Action _abortedCallback = AbortConnection; - private static readonly Base64Encoder Base64Encoder = new Base64Encoder(); - private static readonly PassThroughEncoder PassThroughEncoder = new PassThroughEncoder(); private readonly ConnectionContext _connectionContext; private readonly ILogger _logger; @@ -62,7 +58,7 @@ namespace Microsoft.AspNetCore.SignalR public string UserIdentifier { get; private set; } - internal virtual HubProtocolReaderWriter ProtocolReaderWriter { get; set; } + internal virtual IHubProtocol Protocol { get; set; } internal ExceptionDispatchInfo AbortException { get; private set; } @@ -85,7 +81,7 @@ namespace Microsoft.AspNetCore.SignalR { // This will internally cache the buffer for each unique HubProtocol/DataEncoder combination // So that we don't serialize the HubMessage for every single connection - var buffer = message.WriteMessage(ProtocolReaderWriter); + var buffer = message.WriteMessage(Protocol); _connectionContext.Transport.Output.Write(buffer); Interlocked.Exchange(ref _lastSendTimestamp, Stopwatch.GetTimestamp()); @@ -156,27 +152,25 @@ namespace Microsoft.AspNetCore.SignalR { if (NegotiationProtocol.TryParseMessage(buffer, out var negotiationMessage, out consumed, out examined)) { - var protocol = protocolResolver.GetProtocol(negotiationMessage.Protocol, supportedProtocols, this); + Protocol = protocolResolver.GetProtocol(negotiationMessage.Protocol, supportedProtocols, this); - var transportCapabilities = Features.Get()?.TransportCapabilities - ?? throw new InvalidOperationException("Unable to read transport capabilities."); + // 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. + var transferFormatFeature = Features.Get(); + if (transferFormatFeature != null) + { + if ((transferFormatFeature.SupportedFormats & Protocol.TransferFormat) == 0) + { + throw new InvalidOperationException($"Cannot use the '{Protocol.Name}' protocol on the current transport. The transport does not support the '{Protocol.TransferFormat}' transfer mode."); + } - var dataEncoder = (protocol.Type == ProtocolType.Binary && (transportCapabilities & TransferMode.Binary) == 0) - ? (IDataEncoder)Base64Encoder - : PassThroughEncoder; + transferFormatFeature.ActiveFormat = Protocol.TransferFormat; + } - var transferModeFeature = Features.Get() ?? - throw new InvalidOperationException("Unable to read transfer mode."); + _cachedPingMessage = Protocol.WriteToArray(PingMessage.Instance); - transferModeFeature.TransferMode = - (protocol.Type == ProtocolType.Binary && (transportCapabilities & TransferMode.Binary) != 0) - ? TransferMode.Binary - : TransferMode.Text; - - ProtocolReaderWriter = new HubProtocolReaderWriter(protocol, dataEncoder); - _cachedPingMessage = ProtocolReaderWriter.WriteMessage(PingMessage.Instance); - - Log.UsingHubProtocol(_logger, protocol.Name); + Log.UsingHubProtocol(_logger, Protocol.Name); UserIdentifier = userIdProvider.GetUserId(this); diff --git a/src/Microsoft.AspNetCore.SignalR.Core/HubEndPoint.cs b/src/Microsoft.AspNetCore.SignalR.Core/HubEndPoint.cs index 6a544fb81f..f32c39aad0 100644 --- a/src/Microsoft.AspNetCore.SignalR.Core/HubEndPoint.cs +++ b/src/Microsoft.AspNetCore.SignalR.Core/HubEndPoint.cs @@ -2,10 +2,13 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Buffers; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Protocols; using Microsoft.AspNetCore.SignalR.Core; using Microsoft.AspNetCore.SignalR.Internal; +using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -141,7 +144,10 @@ namespace Microsoft.AspNetCore.SignalR { if (!buffer.IsEmpty) { - if (connection.ProtocolReaderWriter.ReadMessages(buffer, _dispatcher, out var hubMessages, out consumed, out examined)) + var hubMessages = new List(); + + // TODO: Make this incremental + if (connection.Protocol.TryParseMessages(buffer.ToArray(), _dispatcher, hubMessages)) { foreach (var hubMessage in hubMessages) { 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 5f91fef139..94370f1831 100644 --- a/src/Microsoft.AspNetCore.SignalR.Protocols.MsgPack/Internal/Protocol/MessagePackHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Protocols.MsgPack/Internal/Protocol/MessagePackHubProtocol.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Runtime.ExceptionServices; using Microsoft.AspNetCore.SignalR.Internal.Formatters; +using Microsoft.AspNetCore.Sockets; using Microsoft.Extensions.Options; using MsgPack; using MsgPack.Serialization; @@ -24,7 +25,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol public string Name => ProtocolName; - public ProtocolType Type => ProtocolType.Binary; + public TransferFormat TransferFormat => TransferFormat.Binary; public MessagePackHubProtocol() : this(Options.Create(new MessagePackHubProtocolOptions())) diff --git a/src/Microsoft.AspNetCore.Sockets.Abstractions/Features/IConnectionTransportFeature.cs b/src/Microsoft.AspNetCore.Sockets.Abstractions/Features/IConnectionTransportFeature.cs index 3d2a412a4e..58114d438e 100644 --- a/src/Microsoft.AspNetCore.Sockets.Abstractions/Features/IConnectionTransportFeature.cs +++ b/src/Microsoft.AspNetCore.Sockets.Abstractions/Features/IConnectionTransportFeature.cs @@ -1,4 +1,4 @@ -// 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. using System.IO.Pipelines; @@ -8,7 +8,5 @@ namespace Microsoft.AspNetCore.Sockets.Features public interface IConnectionTransportFeature { IDuplexPipe Transport { get; set; } - - TransferMode TransportCapabilities { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Sockets.Abstractions/Features/ITransferFormatFeature.cs b/src/Microsoft.AspNetCore.Sockets.Abstractions/Features/ITransferFormatFeature.cs new file mode 100644 index 0000000000..b6faf614fd --- /dev/null +++ b/src/Microsoft.AspNetCore.Sockets.Abstractions/Features/ITransferFormatFeature.cs @@ -0,0 +1,11 @@ +// 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. + +namespace Microsoft.AspNetCore.Sockets.Features +{ + public interface ITransferFormatFeature + { + TransferFormat SupportedFormats { get; } + TransferFormat ActiveFormat { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Sockets.Abstractions/Features/ITransferModeFeature.cs b/src/Microsoft.AspNetCore.Sockets.Abstractions/Features/ITransferModeFeature.cs deleted file mode 100644 index 2962559a19..0000000000 --- a/src/Microsoft.AspNetCore.Sockets.Abstractions/Features/ITransferModeFeature.cs +++ /dev/null @@ -1,10 +0,0 @@ -// 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. - -namespace Microsoft.AspNetCore.Sockets.Features -{ - public interface ITransferModeFeature - { - TransferMode TransferMode { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.Sockets.Abstractions/IConnection.cs b/src/Microsoft.AspNetCore.Sockets.Abstractions/IConnection.cs index 6726927b35..76b5100b6b 100644 --- a/src/Microsoft.AspNetCore.Sockets.Abstractions/IConnection.cs +++ b/src/Microsoft.AspNetCore.Sockets.Abstractions/IConnection.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Sockets.Client { public interface IConnection { - Task StartAsync(); + Task StartAsync(TransferFormat transferFormat); Task SendAsync(byte[] data, CancellationToken cancellationToken); Task StopAsync(); Task DisposeAsync(); diff --git a/src/Microsoft.AspNetCore.Sockets.Abstractions/TransferMode.cs b/src/Microsoft.AspNetCore.Sockets.Abstractions/TransferFormat.cs similarity index 73% rename from src/Microsoft.AspNetCore.Sockets.Abstractions/TransferMode.cs rename to src/Microsoft.AspNetCore.Sockets.Abstractions/TransferFormat.cs index bd9adc3838..cbec2913d3 100644 --- a/src/Microsoft.AspNetCore.Sockets.Abstractions/TransferMode.cs +++ b/src/Microsoft.AspNetCore.Sockets.Abstractions/TransferFormat.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -6,7 +6,7 @@ using System; namespace Microsoft.AspNetCore.Sockets { [Flags] - public enum TransferMode + public enum TransferFormat { Binary = 0x01, Text = 0x02 diff --git a/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.Log.cs b/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.Log.cs index 7108d31a1f..223a7caa2f 100644 --- a/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.Log.cs +++ b/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.Log.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -85,6 +85,18 @@ namespace Microsoft.AspNetCore.Sockets.Client private static readonly Action _connectionStateChanged = LoggerMessage.Define(LogLevel.Debug, new EventId(25, "ConnectionStateChanged"), "Connection state changed from {previousState} to {newState}."); + private static readonly Action _transportNotSupported = + LoggerMessage.Define(LogLevel.Debug, new EventId(26, "TransportNotSupported"), "Skipping transport {transportName} because it is not supported by this client."); + + private static readonly Action _transportDoesNotSupportTransferFormat = + LoggerMessage.Define(LogLevel.Debug, new EventId(27, "TransportDoesNotSupportTransferFormat"), "Skipping transport {transportName} because it does not support the requested transfer format '{transferFormat}'."); + + private static readonly Action _transportDisabledByClient = + LoggerMessage.Define(LogLevel.Debug, new EventId(28, "TransportDisabledByClient"), "Skipping transport {transportName} because it was disabled by the client."); + + private static readonly Action _transportFailed = + LoggerMessage.Define(LogLevel.Debug, new EventId(29, "TransportFailed"), "Skipping transport {transportName} because it failed to initialize."); + public static void HttpConnectionStarting(ILogger logger) { _httpConnectionStarting(logger, null); @@ -218,6 +230,35 @@ namespace Microsoft.AspNetCore.Sockets.Client { _errorDuringClosedEvent(logger, exception); } + + public static void TransportNotSupported(ILogger logger, string transport) + { + _transportNotSupported(logger, transport, null); + } + + public static void TransportDoesNotSupportTransferFormat(ILogger logger, TransportType transport, TransferFormat transferFormat) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + _transportDoesNotSupportTransferFormat(logger, transport.ToString(), transferFormat.ToString(), null); + } + } + + public static void TransportDisabledByClient(ILogger logger, TransportType transport) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + _transportDisabledByClient(logger, transport.ToString(), null); + } + } + + public static void TransportFailed(ILogger logger, TransportType transport, Exception ex) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + _transportFailed(logger, transport.ToString(), ex); + } + } } } } diff --git a/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.cs b/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.cs index 482a0997d5..e2600847e2 100644 --- a/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.cs +++ b/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.cs @@ -6,13 +6,13 @@ using System.Buffers; using System.Collections.Generic; using System.IO; using System.IO.Pipelines; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Sockets.Client.Http; using Microsoft.AspNetCore.Sockets.Client.Internal; -using Microsoft.AspNetCore.Sockets.Features; using Microsoft.AspNetCore.Sockets.Http.Internal; using Microsoft.AspNetCore.Sockets.Internal; using Microsoft.Extensions.Logging; @@ -47,9 +47,6 @@ namespace Microsoft.AspNetCore.Sockets.Client private PipeWriter Output => _transportChannel.Output; private readonly List _callbacks = new List(); private readonly TransportType _requestedTransportType = TransportType.All; - private TransportType _serverTransports = TransportType.All; - // The order of the transports here is the order determines the fallback order. - private static readonly TransportType[] AllTransports = new[]{ TransportType.WebSockets, TransportType.ServerSentEvents, TransportType.LongPolling }; private readonly ConnectionLogScope _logScope; private readonly IDisposable _scopeDisposable; @@ -153,9 +150,10 @@ namespace Microsoft.AspNetCore.Sockets.Client _scopeDisposable = _logger.BeginScope(_logScope); } - public async Task StartAsync() => await StartAsyncCore().ForceAsync(); + public Task StartAsync() => StartAsync(TransferFormat.Binary); + public async Task StartAsync(TransferFormat transferFormat) => await StartAsyncCore(transferFormat).ForceAsync(); - private Task StartAsyncCore() + private Task StartAsyncCore(TransferFormat transferFormat) { if (ChangeState(from: ConnectionState.Disconnected, to: ConnectionState.Connecting) != ConnectionState.Disconnected) { @@ -166,7 +164,7 @@ namespace Microsoft.AspNetCore.Sockets.Client _startTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _eventQueue = new TaskQueue(); - StartAsyncInternal() + StartAsyncInternal(transferFormat) .ContinueWith(t => { var abortException = _abortException; @@ -195,7 +193,7 @@ namespace Microsoft.AspNetCore.Sockets.Client return negotiationResponse; } - private async Task StartAsyncInternal() + private async Task StartAsyncInternal(TransferFormat transferFormat) { Log.HttpConnectionStarting(_logger); @@ -205,7 +203,7 @@ namespace Microsoft.AspNetCore.Sockets.Client if (_requestedTransportType == TransportType.WebSockets) { Log.StartingTransport(_logger, _requestedTransportType, connectUrl); - await StartTransport(connectUrl, _requestedTransportType); + await StartTransport(connectUrl, _requestedTransportType, transferFormat); } else { @@ -219,14 +217,32 @@ namespace Microsoft.AspNetCore.Sockets.Client } // This should only need to happen once - _serverTransports = GetAvailableServerTransports(negotiationResponse); connectUrl = CreateConnectUrl(Url, negotiationResponse.ConnectionId); - foreach (var transport in AllTransports) + // We're going to search for the transfer format as a string because we don't want to parse + // all the transfer formats in the negotiation response, and we want to allow transfer formats + // we don't understand in the negotiate response. + var transferFormatString = transferFormat.ToString(); + + foreach (var transport in negotiationResponse.AvailableTransports) { + if (!Enum.TryParse(transport.Transport, out var transportType)) + { + Log.TransportNotSupported(_logger, transport.Transport); + continue; + } + try { - if ((transport & _serverTransports & _requestedTransportType) != 0) + if ((transportType & _requestedTransportType) == 0) + { + Log.TransportDisabledByClient(_logger, transportType); + } + else if (!transport.TransferFormats.Contains(transferFormatString, StringComparer.Ordinal)) + { + Log.TransportDoesNotSupportTransferFormat(_logger, transportType, transferFormat); + } + else { // The negotiation response gets cleared in the fallback scenario. if (negotiationResponse == null) @@ -235,13 +251,14 @@ namespace Microsoft.AspNetCore.Sockets.Client connectUrl = CreateConnectUrl(Url, negotiationResponse.ConnectionId); } - Log.StartingTransport(_logger, transport, connectUrl); - await StartTransport(connectUrl, transport); + Log.StartingTransport(_logger, transportType, connectUrl); + await StartTransport(connectUrl, transportType, transferFormat); break; } } - catch (Exception) + catch (Exception ex) { + Log.TransportFailed(_logger, transportType, ex); // Try the next transport // Clear the negotiation response so we know to re-negotiate. negotiationResponse = null; @@ -382,22 +399,6 @@ namespace Microsoft.AspNetCore.Sockets.Client return negotiationResponse; } - private TransportType GetAvailableServerTransports(NegotiationResponse negotiationResponse) - { - if (negotiationResponse.AvailableTransports == null) - { - throw new FormatException("No transports returned in negotiation response."); - } - - var availableServerTransports = (TransportType)0; - foreach (var t in negotiationResponse.AvailableTransports) - { - availableServerTransports |= t; - } - - return availableServerTransports; - } - private static Uri CreateConnectUrl(Uri url, string connectionId) { if (string.IsNullOrWhiteSpace(connectionId)) @@ -408,9 +409,8 @@ namespace Microsoft.AspNetCore.Sockets.Client return Utils.AppendQueryString(url, "id=" + connectionId); } - private async Task StartTransport(Uri connectUrl, TransportType transportType) + private async Task StartTransport(Uri connectUrl, TransportType transportType, TransferFormat transferFormat) { - var options = new PipeOptions(writerScheduler: PipeScheduler.Inline, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); _transportChannel = pair.Transport; @@ -419,15 +419,7 @@ namespace Microsoft.AspNetCore.Sockets.Client // Start the transport, giving it one end of the pipeline try { - await _transport.StartAsync(connectUrl, pair.Application, GetTransferMode(), this); - - // actual transfer mode can differ from the one that was requested so set it on the feature - if (!_transport.Mode.HasValue) - { - // This can happen with custom transports so it should be an exception, not an assert. - throw new InvalidOperationException("Transport was expected to set the Mode property after StartAsync, but it has not been set."); - } - SetTransferMode(_transport.Mode.Value); + await _transport.StartAsync(connectUrl, pair.Application, transferFormat, this); } catch (Exception ex) { @@ -437,29 +429,6 @@ namespace Microsoft.AspNetCore.Sockets.Client } } - private TransferMode GetTransferMode() - { - var transferModeFeature = Features.Get(); - if (transferModeFeature == null) - { - return TransferMode.Text; - } - - return transferModeFeature.TransferMode; - } - - private void SetTransferMode(TransferMode transferMode) - { - var transferModeFeature = Features.Get(); - if (transferModeFeature == null) - { - transferModeFeature = new TransferModeFeature(); - Features.Set(transferModeFeature); - } - - transferModeFeature.TransferMode = transferMode; - } - private async Task ReceiveAsync() { try @@ -753,7 +722,13 @@ namespace Microsoft.AspNetCore.Sockets.Client private class NegotiationResponse { public string ConnectionId { get; set; } - public TransportType[] AvailableTransports { get; set; } + public AvailableTransport[] AvailableTransports { get; set; } + } + + private class AvailableTransport + { + public string Transport { get; set; } + public string[] TransferFormats { get; set; } } } } diff --git a/src/Microsoft.AspNetCore.Sockets.Client.Http/ITransport.cs b/src/Microsoft.AspNetCore.Sockets.Client.Http/ITransport.cs index a01da0c378..3b2efd8aef 100644 --- a/src/Microsoft.AspNetCore.Sockets.Client.Http/ITransport.cs +++ b/src/Microsoft.AspNetCore.Sockets.Client.Http/ITransport.cs @@ -9,8 +9,7 @@ namespace Microsoft.AspNetCore.Sockets.Client { public interface ITransport { - Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection); + Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection); Task StopAsync(); - TransferMode? Mode { get; } } } diff --git a/src/Microsoft.AspNetCore.Sockets.Client.Http/LongPollingTransport.Log.cs b/src/Microsoft.AspNetCore.Sockets.Client.Http/LongPollingTransport.Log.cs index 45497dac89..5eaaf9a369 100644 --- a/src/Microsoft.AspNetCore.Sockets.Client.Http/LongPollingTransport.Log.cs +++ b/src/Microsoft.AspNetCore.Sockets.Client.Http/LongPollingTransport.Log.cs @@ -10,8 +10,8 @@ namespace Microsoft.AspNetCore.Sockets.Client { private static class Log { - private static readonly Action _startTransport = - LoggerMessage.Define(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferMode}."); + private static readonly Action _startTransport = + LoggerMessage.Define(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferFormat}."); private static readonly Action _transportStopped = LoggerMessage.Define(LogLevel.Debug, new EventId(2, "TransportStopped"), "Transport stopped."); @@ -39,9 +39,9 @@ namespace Microsoft.AspNetCore.Sockets.Client // EventIds 100 - 106 used in SendUtils - public static void StartTransport(ILogger logger, TransferMode transferMode) + public static void StartTransport(ILogger logger, TransferFormat transferFormat) { - _startTransport(logger, transferMode, null); + _startTransport(logger, transferFormat, null); } public static void TransportStopped(ILogger logger, Exception exception) diff --git a/src/Microsoft.AspNetCore.Sockets.Client.Http/LongPollingTransport.cs b/src/Microsoft.AspNetCore.Sockets.Client.Http/LongPollingTransport.cs index 76f8dfdb3f..aa64dd9d9d 100644 --- a/src/Microsoft.AspNetCore.Sockets.Client.Http/LongPollingTransport.cs +++ b/src/Microsoft.AspNetCore.Sockets.Client.Http/LongPollingTransport.cs @@ -27,8 +27,6 @@ namespace Microsoft.AspNetCore.Sockets.Client public Task Running { get; private set; } = Task.CompletedTask; - public TransferMode? Mode { get; private set; } - public LongPollingTransport(HttpClient httpClient) : this(httpClient, null, null) { } @@ -40,19 +38,18 @@ namespace Microsoft.AspNetCore.Sockets.Client _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); } - public Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection) + public Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection) { - if (requestedTransferMode != TransferMode.Binary && requestedTransferMode != TransferMode.Text) + if (transferFormat != TransferFormat.Binary && transferFormat != TransferFormat.Text) { - throw new ArgumentException("Invalid transfer mode.", nameof(requestedTransferMode)); + throw new ArgumentException($"The '{transferFormat}' transfer format is not supported by this transport.", nameof(transferFormat)); } connection.Features.Set(new ConnectionInherentKeepAliveFeature(_httpClient.Timeout)); _application = application; - Mode = requestedTransferMode; - Log.StartTransport(_logger, Mode.Value); + Log.StartTransport(_logger, transferFormat); // Start sending and polling (ask for binary if the server supports it) _poller = Poll(url, _transportCts.Token); diff --git a/src/Microsoft.AspNetCore.Sockets.Client.Http/ServerSentEventsTransport.Log.cs b/src/Microsoft.AspNetCore.Sockets.Client.Http/ServerSentEventsTransport.Log.cs index 810f9aa745..7cdfe06acb 100644 --- a/src/Microsoft.AspNetCore.Sockets.Client.Http/ServerSentEventsTransport.Log.cs +++ b/src/Microsoft.AspNetCore.Sockets.Client.Http/ServerSentEventsTransport.Log.cs @@ -10,8 +10,8 @@ namespace Microsoft.AspNetCore.Sockets.Client { private static class Log { - private static readonly Action _startTransport = - LoggerMessage.Define(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferMode}."); + private static readonly Action _startTransport = + LoggerMessage.Define(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferFormat}."); private static readonly Action _transportStopped = LoggerMessage.Define(LogLevel.Debug, new EventId(2, "TransportStopped"), "Transport stopped."); @@ -39,9 +39,9 @@ namespace Microsoft.AspNetCore.Sockets.Client // EventIds 100 - 106 used in SendUtils - public static void StartTransport(ILogger logger, TransferMode transferMode) + public static void StartTransport(ILogger logger, TransferFormat transferFormat) { - _startTransport(logger, transferMode, null); + _startTransport(logger, transferFormat, null); } public static void TransportStopped(ILogger logger, Exception exception) diff --git a/src/Microsoft.AspNetCore.Sockets.Client.Http/ServerSentEventsTransport.cs b/src/Microsoft.AspNetCore.Sockets.Client.Http/ServerSentEventsTransport.cs index e2646041a2..84ad6fac2e 100644 --- a/src/Microsoft.AspNetCore.Sockets.Client.Http/ServerSentEventsTransport.cs +++ b/src/Microsoft.AspNetCore.Sockets.Client.Http/ServerSentEventsTransport.cs @@ -26,8 +26,6 @@ namespace Microsoft.AspNetCore.Sockets.Client public Task Running { get; private set; } = Task.CompletedTask; - public TransferMode? Mode { get; private set; } - public ServerSentEventsTransport(HttpClient httpClient) : this(httpClient, null, null) { } @@ -44,17 +42,16 @@ namespace Microsoft.AspNetCore.Sockets.Client _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); } - public Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection) + public Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection) { - if (requestedTransferMode != TransferMode.Binary && requestedTransferMode != TransferMode.Text) + if (transferFormat != TransferFormat.Text) { - throw new ArgumentException("Invalid transfer mode.", nameof(requestedTransferMode)); + throw new ArgumentException($"The '{transferFormat}' transfer format is not supported by this transport.", nameof(transferFormat)); } _application = application; - Mode = TransferMode.Text; // Server Sent Events is a text only transport - Log.StartTransport(_logger, Mode.Value); + Log.StartTransport(_logger, transferFormat); var sendTask = SendUtils.SendMessages(url, _application, _httpClient, _httpOptions, _transportCts, _logger); var receiveTask = OpenConnection(_application, url, _transportCts.Token); diff --git a/src/Microsoft.AspNetCore.Sockets.Client.Http/TransferModeFeature.cs b/src/Microsoft.AspNetCore.Sockets.Client.Http/TransferModeFeature.cs deleted file mode 100644 index 77f5697a01..0000000000 --- a/src/Microsoft.AspNetCore.Sockets.Client.Http/TransferModeFeature.cs +++ /dev/null @@ -1,12 +0,0 @@ -// 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. - -using Microsoft.AspNetCore.Sockets.Features; - -namespace Microsoft.AspNetCore.Sockets.Client -{ - public class TransferModeFeature : ITransferModeFeature - { - public TransferMode TransferMode { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.Sockets.Client.Http/WebSocketsTransport.Log.cs b/src/Microsoft.AspNetCore.Sockets.Client.Http/WebSocketsTransport.Log.cs index 87532f0d63..68f01dfecf 100644 --- a/src/Microsoft.AspNetCore.Sockets.Client.Http/WebSocketsTransport.Log.cs +++ b/src/Microsoft.AspNetCore.Sockets.Client.Http/WebSocketsTransport.Log.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -11,8 +11,8 @@ namespace Microsoft.AspNetCore.Sockets.Client { private static class Log { - private static readonly Action _startTransport = - LoggerMessage.Define(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferMode}."); + private static readonly Action _startTransport = + LoggerMessage.Define(LogLevel.Information, new EventId(1, "StartTransport"), "Starting transport. Transfer mode: {transferFormat}."); private static readonly Action _transportStopped = LoggerMessage.Define(LogLevel.Debug, new EventId(2, "TransportStopped"), "Transport stopped."); @@ -65,9 +65,9 @@ namespace Microsoft.AspNetCore.Sockets.Client private static readonly Action _cancelMessage = LoggerMessage.Define(LogLevel.Debug, new EventId(18, "CancelMessage"), "Canceled passing message to application."); - public static void StartTransport(ILogger logger, TransferMode transferMode) + public static void StartTransport(ILogger logger, TransferFormat transferFormat) { - _startTransport(logger, transferMode, null); + _startTransport(logger, transferFormat, null); } public static void TransportStopped(ILogger logger, Exception exception) diff --git a/src/Microsoft.AspNetCore.Sockets.Client.Http/WebSocketsTransport.cs b/src/Microsoft.AspNetCore.Sockets.Client.Http/WebSocketsTransport.cs index 6464d089cc..e8c30cb08f 100644 --- a/src/Microsoft.AspNetCore.Sockets.Client.Http/WebSocketsTransport.cs +++ b/src/Microsoft.AspNetCore.Sockets.Client.Http/WebSocketsTransport.cs @@ -19,14 +19,13 @@ namespace Microsoft.AspNetCore.Sockets.Client { private readonly ClientWebSocket _webSocket; private IDuplexPipe _application; + private WebSocketMessageType _webSocketMessageType; private readonly ILogger _logger; private readonly TimeSpan _closeTimeout; private volatile bool _aborted; public Task Running { get; private set; } = Task.CompletedTask; - public TransferMode? Mode { get; private set; } - public WebSocketsTransport() : this(null, null) { @@ -87,7 +86,7 @@ namespace Microsoft.AspNetCore.Sockets.Client _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); } - public async Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection) + public async Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection) { if (url == null) { @@ -99,15 +98,18 @@ namespace Microsoft.AspNetCore.Sockets.Client throw new ArgumentNullException(nameof(application)); } - if (requestedTransferMode != TransferMode.Binary && requestedTransferMode != TransferMode.Text) + if (transferFormat != TransferFormat.Binary && transferFormat != TransferFormat.Text) { - throw new ArgumentException("Invalid transfer mode.", nameof(requestedTransferMode)); + throw new ArgumentException($"The '{transferFormat}' transfer format is not supported by this transport.", nameof(transferFormat)); } _application = application; - Mode = requestedTransferMode; + _webSocketMessageType = transferFormat == TransferFormat.Binary + ? WebSocketMessageType.Binary + : WebSocketMessageType.Text; - Log.StartTransport(_logger, Mode.Value); + + Log.StartTransport(_logger, transferFormat); await Connect(url); @@ -243,11 +245,6 @@ namespace Microsoft.AspNetCore.Sockets.Client private async Task StartSending(WebSocket socket) { - var webSocketMessageType = - Mode == TransferMode.Binary - ? WebSocketMessageType.Binary - : WebSocketMessageType.Text; - Exception error = null; try @@ -274,7 +271,7 @@ namespace Microsoft.AspNetCore.Sockets.Client if (WebSocketCanSend(socket)) { - await socket.SendAsync(buffer, webSocketMessageType); + await socket.SendAsync(buffer, _webSocketMessageType); } else { diff --git a/src/Microsoft.AspNetCore.Sockets.Http/HttpConnectionDispatcher.cs b/src/Microsoft.AspNetCore.Sockets.Http/HttpConnectionDispatcher.cs index f3f6eaa047..54bcd7d94b 100644 --- a/src/Microsoft.AspNetCore.Sockets.Http/HttpConnectionDispatcher.cs +++ b/src/Microsoft.AspNetCore.Sockets.Http/HttpConnectionDispatcher.cs @@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Sockets Log.EstablishedConnection(_logger); // ServerSentEvents is a text protocol only - connection.TransportCapabilities = TransferMode.Text; + connection.SupportedFormats = TransferFormat.Text; // We only need to provide the Input channel since writing to the application is handled through /send. var sse = new ServerSentEventsTransport(connection.Application.Input, connection.ConnectionId, _loggerFactory); @@ -389,15 +389,15 @@ namespace Microsoft.AspNetCore.Sockets jsonWriter.WriteStartArray(); if ((options.Transports & TransportType.WebSockets) != 0) { - jsonWriter.WriteValue(nameof(TransportType.WebSockets)); + WriteTransport(jsonWriter, nameof(TransportType.WebSockets), TransferFormat.Text | TransferFormat.Binary); } if ((options.Transports & TransportType.ServerSentEvents) != 0) { - jsonWriter.WriteValue(nameof(TransportType.ServerSentEvents)); + WriteTransport(jsonWriter, nameof(TransportType.ServerSentEvents), TransferFormat.Text); } if ((options.Transports & TransportType.LongPolling) != 0) { - jsonWriter.WriteValue(nameof(TransportType.LongPolling)); + WriteTransport(jsonWriter, nameof(TransportType.LongPolling), TransferFormat.Text | TransferFormat.Binary); } jsonWriter.WriteEndArray(); jsonWriter.WriteEndObject(); @@ -406,6 +406,27 @@ namespace Microsoft.AspNetCore.Sockets return sb.ToString(); } + private static void WriteTransport(JsonWriter writer, string transportName, TransferFormat supportedTransferFormats) + { + writer.WriteStartObject(); + writer.WritePropertyName("transport"); + writer.WriteValue(transportName); + writer.WritePropertyName("transferFormats"); + writer.WriteStartArray(); + if ((supportedTransferFormats & TransferFormat.Binary) != 0) + { + writer.WriteValue(nameof(TransferFormat.Binary)); + } + + if ((supportedTransferFormats & TransferFormat.Text) != 0) + { + writer.WriteValue(nameof(TransferFormat.Text)); + } + + writer.WriteEndArray(); + writer.WriteEndObject(); + } + private static string GetConnectionId(HttpContext context) => context.Request.Query["id"]; private async Task ProcessSend(HttpContext context) @@ -477,9 +498,6 @@ namespace Microsoft.AspNetCore.Sockets connection.User = context.User; connection.SetHttpContext(context); - // this is the default setting which should be overwritten by transports that have different capabilities (e.g. SSE) - connection.TransportCapabilities = TransferMode.Binary | TransferMode.Text; - // Set the Connection ID on the logging scope so that logs from now on will have the // Connection ID metadata set. logScope.ConnectionId = connection.ConnectionId; diff --git a/src/Microsoft.AspNetCore.Sockets.Http/Internal/Transports/WebSocketsTransport.cs b/src/Microsoft.AspNetCore.Sockets.Http/Internal/Transports/WebSocketsTransport.cs index b054fb6c49..db53e5cd9a 100644 --- a/src/Microsoft.AspNetCore.Sockets.Http/Internal/Transports/WebSocketsTransport.cs +++ b/src/Microsoft.AspNetCore.Sockets.Http/Internal/Transports/WebSocketsTransport.cs @@ -220,7 +220,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Transports { Log.SendPayload(_logger, buffer.Length); - var webSocketMessageType = (_connection.TransferMode == TransferMode.Binary + var webSocketMessageType = (_connection.ActiveFormat == TransferFormat.Binary ? WebSocketMessageType.Binary : WebSocketMessageType.Text); diff --git a/src/Microsoft.AspNetCore.Sockets/DefaultConnectionContext.cs b/src/Microsoft.AspNetCore.Sockets/DefaultConnectionContext.cs index d523335e9e..9a6614a958 100644 --- a/src/Microsoft.AspNetCore.Sockets/DefaultConnectionContext.cs +++ b/src/Microsoft.AspNetCore.Sockets/DefaultConnectionContext.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Buffers; using System.Collections.Generic; using System.IO.Pipelines; using System.Security.Claims; @@ -21,7 +20,7 @@ namespace Microsoft.AspNetCore.Sockets IConnectionTransportFeature, IConnectionUserFeature, IConnectionHeartbeatFeature, - ITransferModeFeature + ITransferFormatFeature { private List<(Action handler, object state)> _heartbeatHandlers = new List<(Action handler, object state)>(); @@ -37,14 +36,18 @@ namespace Microsoft.AspNetCore.Sockets ConnectionId = id; LastSeenUtc = DateTime.UtcNow; + // The default behavior is that both formats are supported. + SupportedFormats = TransferFormat.Binary | TransferFormat.Text; + ActiveFormat = TransferFormat.Text; + // PERF: This type could just implement IFeatureCollection Features = new FeatureCollection(); Features.Set(this); Features.Set(this); Features.Set(this); Features.Set(this); - Features.Set(this); Features.Set(this); + Features.Set(this); } public CancellationTokenSource Cancellation { get; set; } @@ -71,9 +74,9 @@ namespace Microsoft.AspNetCore.Sockets public override IDuplexPipe Transport { get; set; } - public TransferMode TransportCapabilities { get; set; } + public TransferFormat SupportedFormats { get; set; } - public TransferMode TransferMode { get; set; } + public TransferFormat ActiveFormat { get; set; } public void OnHeartbeat(Action action, object state) { diff --git a/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/HubConnectionTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/HubConnectionTests.cs index 1e8d72cbe4..5b721360b8 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/HubConnectionTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/HubConnectionTests.cs @@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests { await connection.StartAsync().OrTimeout(); - var result = await connection.InvokeAsync("HelloWorld").OrTimeout(); + var result = await connection.InvokeAsync(nameof(TestHub.HelloWorld)).OrTimeout(); Assert.Equal("Hello World!", result); } @@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests { await connection.StartAsync().OrTimeout(); - var result = await connection.InvokeAsync("Echo", originalMessage).OrTimeout(); + var result = await connection.InvokeAsync(nameof(TestHub.Echo), originalMessage).OrTimeout(); Assert.Equal(originalMessage, result); } @@ -115,11 +115,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests try { await connection.StartAsync().OrTimeout(); - var result = await connection.InvokeAsync("Echo", originalMessage).OrTimeout(); + var result = await connection.InvokeAsync(nameof(TestHub.Echo), originalMessage).OrTimeout(); Assert.Equal(originalMessage, result); await connection.StopAsync().OrTimeout(); await connection.StartAsync().OrTimeout(); - result = await connection.InvokeAsync("Echo", originalMessage).OrTimeout(); + result = await connection.InvokeAsync(nameof(TestHub.Echo), originalMessage).OrTimeout(); Assert.Equal(originalMessage, result); } catch (Exception ex) @@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests try { await connection.StartAsync().OrTimeout(); - var result = await connection.InvokeAsync("Echo", originalMessage).OrTimeout(); + var result = await connection.InvokeAsync(nameof(TestHub.Echo), originalMessage).OrTimeout(); Assert.Equal(originalMessage, result); logger.LogInformation("Stopping connection"); @@ -169,7 +169,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests await restartTcs.Task.OrTimeout(); logger.LogInformation("Reconnection complete"); - result = await connection.InvokeAsync("Echo", originalMessage).OrTimeout(); + result = await connection.InvokeAsync(nameof(TestHub.Echo), originalMessage).OrTimeout(); Assert.Equal(originalMessage, result); } @@ -199,7 +199,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests { await connection.StartAsync().OrTimeout(); - var result = await connection.InvokeAsync("echo", originalMessage).OrTimeout(); + var result = await connection.InvokeAsync(nameof(TestHub.Echo).ToLowerInvariant(), originalMessage).OrTimeout(); Assert.Equal(originalMessage, result); } @@ -677,7 +677,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests try { await hubConnection.StartAsync().OrTimeout(); - var message = await hubConnection.InvokeAsync("Echo", "Hello, World!").OrTimeout(); + var message = await hubConnection.InvokeAsync(nameof(TestHub.Echo), "Hello, World!").OrTimeout(); Assert.Equal("Hello, World!", message); } catch (Exception ex) @@ -708,7 +708,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests try { await hubConnection.StartAsync().OrTimeout(); - var headerValues = await hubConnection.InvokeAsync("GetHeaderValues", new object[] { new[] { "X-test", "X-42" } }).OrTimeout(); + var headerValues = await hubConnection.InvokeAsync(nameof(TestHub.GetHeaderValues), new object[] { new[] { "X-test", "X-42" } }).OrTimeout(); Assert.Equal(new[] { "42", "test" }, headerValues); } catch (Exception ex) @@ -742,7 +742,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests try { await hubConnection.StartAsync().OrTimeout(); - var cookieValue = await hubConnection.InvokeAsync("GetCookieValue", new object[] { "Foo" }).OrTimeout(); + var cookieValue = await hubConnection.InvokeAsync(nameof(TestHub.GetCookieValue), new object[] { "Foo" }).OrTimeout(); Assert.Equal("Bar", cookieValue); } catch (Exception ex) @@ -772,7 +772,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests { await hubConnection.StartAsync().OrTimeout(); - var features = await hubConnection.InvokeAsync("GetIHttpConnectionFeatureProperties").OrTimeout(); + var features = await hubConnection.InvokeAsync(nameof(TestHub.GetIHttpConnectionFeatureProperties)).OrTimeout(); var localPort = (Int64)features[0]; var remotePort = (Int64)features[1]; var localIP = (string)features[2]; @@ -795,23 +795,56 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests } } + [Fact] + public async Task NegotiationSkipsServerSentEventsWhenUsingBinaryProtocol() + { + using (StartLog(out var loggerFactory)) + { + var hubConnection = new HubConnectionBuilder() + .WithUrl(_serverFixture.Url + "/default-nowebsockets") + .WithHubProtocol(new MessagePackHubProtocol()) + .WithLoggerFactory(loggerFactory) + .Build(); + try + { + await hubConnection.StartAsync().OrTimeout(); + + var transport = await hubConnection.InvokeAsync(nameof(TestHub.GetActiveTransportName)).OrTimeout(); + Assert.Equal(TransportType.LongPolling, transport); + } + catch (Exception ex) + { + loggerFactory.CreateLogger().LogError(ex, "Exception from test"); + throw; + } + finally + { + await hubConnection.DisposeAsync().OrTimeout(); + } + } + } + public static IEnumerable HubProtocolsAndTransportsAndHubPaths { get { foreach (var protocol in HubProtocols) { - foreach (var transport in TransportTypes().SelectMany(t => t)) + foreach (var transport in TransportTypes().SelectMany(t => t).Cast()) { foreach (var hubPath in HubPaths) { - yield return new object[] { protocol, transport, hubPath }; + if (!(protocol is MessagePackHubProtocol) || transport != TransportType.ServerSentEvents) + { + yield return new object[] { protocol, transport, hubPath }; + } } } } } } + // This list excludes "special" hub paths like "default-nowebsockets" which exist for specific tests. public static string[] HubPaths = new[] { "/default", "/dynamic", "/hubT" }; public static IEnumerable HubProtocols => diff --git a/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/Hubs.cs b/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/Hubs.cs index 6b9545d8a0..6928a52fff 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/Hubs.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/Hubs.cs @@ -9,6 +9,7 @@ using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Sockets; namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests { @@ -57,6 +58,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests return result; } + + public string GetActiveTransportName() + { + return Context.Connection.Metadata[ConnectionMetadataNames.Transport].ToString(); + } } public class DynamicTestHub : DynamicHub diff --git a/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/Startup.cs b/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/Startup.cs index 3952d62f50..41e05ab74c 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/Startup.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/Startup.cs @@ -7,6 +7,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Sockets; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; @@ -54,6 +55,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests routes.MapHub("/dynamic"); routes.MapHub("/hubT"); routes.MapHub("/authorizedhub"); + routes.MapHub("/default-nowebsockets", options => options.Transports = TransportType.LongPolling | TransportType.ServerSentEvents); }); app.Run(async (context) => diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.AbortAsync.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.AbortAsync.cs index 8ccf329197..54052ef59a 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.AbortAsync.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.AbortAsync.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Sockets; using Xunit; namespace Microsoft.AspNetCore.SignalR.Client.Tests @@ -18,7 +19,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests return WithConnectionAsync(CreateConnection(), async (connection, closed) => { // Start the connection - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); // Abort with an error var expected = new Exception("Ruh roh!"); @@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests return WithConnectionAsync(CreateConnection(transport: new TestTransport(onTransportStop: SyncPoint.Create(2, out var syncPoints))), async (connection, closed) => { // Start the connection - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); // Stop normally var stopTask = connection.StopAsync().OrTimeout(); @@ -70,7 +71,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests return WithConnectionAsync(CreateConnection(transport: new TestTransport(onTransportStop: SyncPoint.Create(2, out var syncPoints))), async (connection, closed) => { // Start the connection - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); // Abort with an error var expected = new Exception("Ruh roh!"); @@ -97,7 +98,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests return WithConnectionAsync(CreateConnection(transport: new TestTransport(onTransportStop: SyncPoint.Create(out var syncPoint))), async (connection, closed) => { // Start the connection - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); // Abort with an error var expected = new Exception("Ruh roh!"); @@ -106,7 +107,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests // Wait to reach the first sync point await syncPoint.WaitForSyncPoint().OrTimeout(); - var ex = await Assert.ThrowsAsync(() => connection.StartAsync().OrTimeout()); + var ex = await Assert.ThrowsAsync(() => connection.StartAsync(TransferFormat.Text).OrTimeout()); Assert.Equal("Cannot start a connection that is not in the Disconnected state.", ex.Message); // Release the sync point and wait for close to complete @@ -117,7 +118,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests Assert.Same(expected, actual); // We can start now - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); // And we can stop without getting the abort exception. await connection.StopAsync().OrTimeout(); diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.ConnectionLifecycle.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.ConnectionLifecycle.cs index 35eb53d59c..b5634b311f 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.ConnectionLifecycle.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.ConnectionLifecycle.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Client.Tests; +using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets.Client; using Microsoft.Extensions.Logging.Testing; using Xunit; @@ -28,10 +29,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests { await WithConnectionAsync(CreateConnection(loggerFactory: loggerFactory), async (connection, closed) => { - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); var exception = await Assert.ThrowsAsync( - async () => await connection.StartAsync().OrTimeout()); + async () => await connection.StartAsync(TransferFormat.Text).OrTimeout()); Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message); }); } @@ -47,11 +48,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests CreateConnection(loggerFactory: loggerFactory), async (connection, closed) => { - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); await connection.DisposeAsync(); var exception = await Assert.ThrowsAsync( - async () => await connection.StartAsync().OrTimeout()); + async () => await connection.StartAsync(TransferFormat.Text).OrTimeout()); Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message); }); @@ -70,7 +71,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests await connection.DisposeAsync(); var exception = await Assert.ThrowsAsync( - async () => await connection.StartAsync().OrTimeout()); + async () => await connection.StartAsync(TransferFormat.Text).OrTimeout()); Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message); }); @@ -91,7 +92,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests async (connection, closed) => { // Start the connection and wait for the transport to start up. - var startTask = connection.StartAsync(); + var startTask = connection.StartAsync(TransferFormat.Text); await transportStart.WaitForSyncPoint().OrTimeout(); // While the transport is starting, dispose the connection @@ -139,7 +140,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests async (connection, closed) => { Assert.Equal(0, startCounter); - await connection.StartAsync(); + await connection.StartAsync(TransferFormat.Text); Assert.Equal(passThreshold, startCounter); }); } @@ -164,7 +165,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests transport: new TestTransport(onTransportStart: OnTransportStart)), async (connection, closed) => { - var ex = await Assert.ThrowsAsync(() => connection.StartAsync()); + var ex = await Assert.ThrowsAsync(() => connection.StartAsync(TransferFormat.Text)); Assert.Equal("Unable to connect to the server with any of the available transports.", ex.Message); Assert.Equal(3, startCounter); }); @@ -180,9 +181,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests CreateConnection(loggerFactory: loggerFactory), async (connection, closed) => { - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); await connection.StopAsync().OrTimeout(); - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); }); } } @@ -199,7 +200,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests async (connection, closed) => { // Start and wait for the transport to start up. - var startTask = connection.StartAsync(); + var startTask = connection.StartAsync(TransferFormat.Text); await transportStart.WaitForSyncPoint().OrTimeout(); // Stop the connection while it's starting @@ -223,7 +224,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests CreateConnection(loggerFactory: loggerFactory), async (connection, closed) => { - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); await Task.WhenAll(connection.StopAsync(), connection.StopAsync()).OrTimeout(); await closed.OrTimeout(); }); @@ -257,14 +258,14 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests CreateConnection(httpHandler, loggerFactory), async (connection, closed) => { - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); await connection.SendAsync(new byte[] { 0x42 }).OrTimeout(); // Wait for the connection to close, because the send failed. await Assert.ThrowsAsync(() => closed.OrTimeout()); // Start it up again - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); }); } } @@ -281,7 +282,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests async (connection, closed) => { // Start the connection - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); // Stop the connection var stopTask = connection.StopAsync().OrTimeout(); @@ -299,7 +300,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests await disposeTask.OrTimeout(); // We should be disposed and thus unable to restart. - var exception = await Assert.ThrowsAsync(() => connection.StartAsync().OrTimeout()); + var exception = await Assert.ThrowsAsync(() => connection.StartAsync(TransferFormat.Text).OrTimeout()); Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message); }); } @@ -314,7 +315,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests CreateConnection(loggerFactory: loggerFactory), async (connection, closed) => { - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); await connection.StopAsync().OrTimeout(); await closed.OrTimeout(); await connection.DisposeAsync().OrTimeout(); @@ -329,7 +330,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests CreateConnection(), async (connection, closed) => { - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); await connection.DisposeAsync().OrTimeout(); await closed.OrTimeout(); }); @@ -346,7 +347,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests CreateConnection(transport: testTransport), async (connection, closed) => { - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); testTransport.Application.Output.Complete(expected); var actual = await Assert.ThrowsAsync(() => closed.OrTimeout()); Assert.Same(expected, actual); @@ -384,7 +385,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests async (connection, closed) => { // Start the transport - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); Assert.False(longPollingTransport.Running.IsCompleted, "Expected that the transport would still be running"); // Stop the connection, and we should stop the transport diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.Helpers.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.Helpers.cs index 6a6441841c..c691020420 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.Helpers.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.Helpers.cs @@ -14,26 +14,33 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests { public partial class HttpConnectionTests { - private static HttpConnection CreateConnection(HttpMessageHandler httpHandler = null, ILoggerFactory loggerFactory = null, string url = null, ITransport transport = null) + private static HttpConnection CreateConnection(HttpMessageHandler httpHandler = null, ILoggerFactory loggerFactory = null, string url = null, ITransport transport = null, ITransportFactory transportFactory = null) { var httpOptions = new HttpOptions() { HttpMessageHandler = (httpMessageHandler) => httpHandler ?? TestHttpMessageHandler.CreateDefault(), }; - return CreateConnection(httpOptions, loggerFactory, url, transport); + return CreateConnection(httpOptions, loggerFactory, url, transport, transportFactory); } - private static HttpConnection CreateConnection(HttpOptions httpOptions, ILoggerFactory loggerFactory = null, string url = null, ITransport transport = null) + private static HttpConnection CreateConnection(HttpOptions httpOptions, ILoggerFactory loggerFactory = null, string url = null, ITransport transport = null, ITransportFactory transportFactory = null) { loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; var uri = new Uri(url ?? "http://fakeuri.org/"); - var connection = (transport != null) ? - new HttpConnection(uri, new TestTransportFactory(transport), loggerFactory, httpOptions) : - new HttpConnection(uri, TransportType.LongPolling, loggerFactory, httpOptions); - - return connection; + if (transportFactory != null) + { + return new HttpConnection(uri, transportFactory, loggerFactory, httpOptions); + } + else if (transport != null) + { + return new HttpConnection(uri, new TestTransportFactory(transport), loggerFactory, httpOptions); + } + else + { + return new HttpConnection(uri, TransportType.LongPolling, loggerFactory, httpOptions); + } } private static async Task WithConnectionAsync(HttpConnection connection, Func body) diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.Negotiate.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.Negotiate.cs index 242bfada92..a654f64afc 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.Negotiate.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.Negotiate.cs @@ -5,6 +5,10 @@ using System; using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Client.Tests; +using Microsoft.AspNetCore.Sockets; +using Microsoft.AspNetCore.Sockets.Client; +using Moq; +using Newtonsoft.Json; using Xunit; using TransportType = Microsoft.AspNetCore.Sockets.TransportType; @@ -32,7 +36,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests [Fact] public Task StartThrowsFormatExceptionIfNegotiationResponseHasNoTransports() { - return RunInvalidNegotiateResponseTest(ResponseUtils.CreateNegotiationContent(transportTypes: null), "No transports returned in negotiation response."); + return RunInvalidNegotiateResponseTest(ResponseUtils.CreateNegotiationContent(transportTypes: 0), "Unable to connect to the server with any of the available transports."); } [Theory] @@ -67,12 +71,104 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests CreateConnection(testHttpHandler, url: requestedUrl), async (connection, closed) => { - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); }); Assert.Equal(expectedNegotiate, await negotiateUrlTcs.Task.OrTimeout()); } + [Fact] + public async Task StartSkipsOverTransportsThatTheClientDoesNotUnderstand() + { + var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false); + + testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent)); + testHttpHandler.OnNegotiate((request, cancellationToken) => + { + return ResponseUtils.CreateResponse(HttpStatusCode.OK, + JsonConvert.SerializeObject(new + { + connectionId = "00000000-0000-0000-0000-000000000000", + availableTransports = new object[] + { + new + { + transport = "QuantumEntanglement", + transferFormats = new string[] { "Qbits" }, + }, + new + { + transport = "CarrierPigeon", + transferFormats = new string[] { "Text" }, + }, + new + { + transport = "LongPolling", + transferFormats = new string[] { "Text", "Binary" } + }, + } + })); + }); + + var transportFactory = new Mock(MockBehavior.Strict); + + transportFactory.Setup(t => t.CreateTransport(TransportType.LongPolling)) + .Returns(new TestTransport(transferFormat: TransferFormat.Text | TransferFormat.Binary)); + + await WithConnectionAsync( + CreateConnection(testHttpHandler, transportFactory: transportFactory.Object), + async (connection, closed) => + { + await connection.StartAsync(TransferFormat.Binary).OrTimeout(); + }); + } + + [Fact] + public async Task StartSkipsOverTransportsThatDoNotSupportTheRequredTransferFormat() + { + var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false); + + testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent)); + testHttpHandler.OnNegotiate((request, cancellationToken) => + { + return ResponseUtils.CreateResponse(HttpStatusCode.OK, + JsonConvert.SerializeObject(new + { + connectionId = "00000000-0000-0000-0000-000000000000", + availableTransports = new object[] + { + new + { + transport = "WebSockets", + transferFormats = new string[] { "Qbits" }, + }, + new + { + transport = "ServerSentEvents", + transferFormats = new string[] { "Text" }, + }, + new + { + transport = "LongPolling", + transferFormats = new string[] { "Text", "Binary" } + }, + } + })); + }); + + var transportFactory = new Mock(MockBehavior.Strict); + + transportFactory.Setup(t => t.CreateTransport(TransportType.LongPolling)) + .Returns(new TestTransport(transferFormat: TransferFormat.Text | TransferFormat.Binary)); + + await WithConnectionAsync( + CreateConnection(testHttpHandler, transportFactory: transportFactory.Object), + async (connection, closed) => + { + await connection.StartAsync(TransferFormat.Binary).OrTimeout(); + }); + } + private async Task RunInvalidNegotiateResponseTest(string negotiatePayload, string expectedExceptionMessage) where TException : Exception { var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false); @@ -84,7 +180,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests async (connection, closed) => { var exception = await Assert.ThrowsAsync( - () => connection.StartAsync().OrTimeout()); + () => connection.StartAsync(TransferFormat.Text).OrTimeout()); Assert.Equal(expectedExceptionMessage, exception.Message); }); diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.OnReceived.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.OnReceived.cs index 91b7a94cb0..da63760462 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.OnReceived.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.OnReceived.cs @@ -6,6 +6,7 @@ using System.Net; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Client.Tests; +using Microsoft.AspNetCore.Sockets; using Xunit; namespace Microsoft.AspNetCore.SignalR.Client.Tests @@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests return Task.CompletedTask; }, receiveTcs); - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); Assert.Contains("42", await receiveTcs.Task.OrTimeout()); }); } @@ -65,7 +66,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests return Task.CompletedTask; }, receiveTcs); - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); Assert.Contains("42", await receiveTcs.Task.OrTimeout()); Assert.True(receivedRaised); }); @@ -97,7 +98,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests return Task.CompletedTask; }, receiveTcs); - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); Assert.Contains("42", await receiveTcs.Task.OrTimeout()); Assert.True(receivedRaised); }); diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.SendAsync.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.SendAsync.cs index 7494a962f0..f3b51cb62f 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.SendAsync.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.SendAsync.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Client.Tests; +using Microsoft.AspNetCore.Sockets; using Xunit; namespace Microsoft.AspNetCore.SignalR.Client.Tests @@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests CreateConnection(testHttpHandler), async (connection, closed) => { - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); await connection.SendAsync(data).OrTimeout(); @@ -66,7 +67,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests CreateConnection(), async (connection, closed) => { - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); await connection.StopAsync().OrTimeout(); var exception = await Assert.ThrowsAsync( @@ -82,7 +83,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests CreateConnection(), async (connection, closed) => { - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); await connection.DisposeAsync().OrTimeout(); var exception = await Assert.ThrowsAsync( @@ -114,7 +115,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests CreateConnection(testHttpHandler), async (connection, closed) => { - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); await connection.SendAsync(new byte[] { 0 }).OrTimeout(); diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.cs index 32bec2a0cf..9286b372b0 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.cs @@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var onReceived = new SyncPoint(); connection.OnReceived(_ => onReceived.WaitToContinue().OrTimeout()); - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); // This will trigger the received callback await testTransport.Application.Output.WriteAsync(new byte[] { 1 }); @@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests connection.OnReceived(_ => onReceived.WaitToContinue().OrTimeout()); logger.LogInformation("Starting connection"); - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); logger.LogInformation("Started connection"); await testTransport.Application.Output.WriteAsync(new byte[] { 1 }); @@ -131,23 +131,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } - [Fact] - public async Task StartAsyncSetsTransferModeFeature() - { - var testTransport = new TestTransport(transferMode: TransferMode.Binary); - await WithConnectionAsync( - CreateConnection(transport: testTransport), - async (connection, closed) => - { - Assert.Null(connection.Features.Get()); - await connection.StartAsync().OrTimeout(); - - var transferModeFeature = connection.Features.Get(); - Assert.NotNull(transferModeFeature); - Assert.Equal(TransferMode.Binary, transferModeFeature.TransferMode); - }); - } - [Fact] public async Task HttpOptionsSetOntoHttpClientHandler() { @@ -180,7 +163,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests CreateConnection(httpOptions, url: "http://fakeuri.org/"), async (connection, closed) => { - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); }); Assert.NotNull(httpClientHandler); diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs index e595f08cf2..490cb5bf4f 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionProtocolTests.cs @@ -324,76 +324,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } - [Fact] - public async Task MessagesEncodedWhenUsingBinaryProtocolOverTextTransport() - { - var connection = new TestConnection(TransferMode.Text); - - var hubConnection = new HubConnection(connection, - new MessagePackHubProtocol(), new LoggerFactory()); - try - { - await hubConnection.StartAsync().OrTimeout(); - await hubConnection.SendAsync("MyMethod", 42).OrTimeout(); - - await connection.ReadSentTextMessageAsync().OrTimeout(); - var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout(); - - // The message is in the following format `size:payload;` - var parts = invokeMessage.Split(':'); - Assert.Equal(2, parts.Length); - Assert.True(int.TryParse(parts[0], out var payloadSize)); - Assert.Equal(payloadSize, parts[1].Length - 1); - Assert.EndsWith(";", parts[1]); - - // this throws if the message is not a valid base64 string - Convert.FromBase64String(parts[1].Substring(0, payloadSize)); - } - finally - { - await hubConnection.DisposeAsync().OrTimeout(); - await connection.DisposeAsync().OrTimeout(); - } - } - - [Fact] - public async Task MessagesDecodedWhenUsingBinaryProtocolOverTextTransport() - { - var connection = new TestConnection(TransferMode.Text); - var hubConnection = new HubConnection(connection, - new MessagePackHubProtocol(), new LoggerFactory()); - - var invocationTcs = new TaskCompletionSource(); - try - { - await hubConnection.StartAsync().OrTimeout(); - hubConnection.On("MyMethod", result => invocationTcs.SetResult(result)); - - using (var ms = new MemoryStream()) - { - new MessagePackHubProtocol() - .WriteMessage(new InvocationMessage(null, "MyMethod", null, 42), ms); - - var invokeMessage = Convert.ToBase64String(ms.ToArray()); - var payloadSize = invokeMessage.Length.ToString(CultureInfo.InvariantCulture); - var message = $"{payloadSize}:{invokeMessage};"; - - connection.ReceivedMessages.TryWrite(Encoding.UTF8.GetBytes(message)); - } - - Assert.Equal(42, await invocationTcs.Task.OrTimeout()); - } - finally - { - await hubConnection.DisposeAsync().OrTimeout(); - await connection.DisposeAsync().OrTimeout(); - } - } - [Fact] public async Task AcceptsPingMessages() { - var connection = new TestConnection(TransferMode.Text); + var connection = new TestConnection(); var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory()); diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs index b876176338..cdd20a25f0 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; 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; @@ -21,12 +22,14 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests public async Task StartAsyncCallsConnectionStart() { var connection = new Mock(); + var protocol = new Mock(); + protocol.SetupGet(p => p.TransferFormat).Returns(TransferFormat.Text); connection.SetupGet(p => p.Features).Returns(new FeatureCollection()); - connection.Setup(m => m.StartAsync()).Returns(Task.CompletedTask).Verifiable(); - var hubConnection = new HubConnection(connection.Object, Mock.Of(), null); + connection.Setup(m => m.StartAsync(TransferFormat.Text)).Returns(Task.CompletedTask).Verifiable(); + var hubConnection = new HubConnection(connection.Object, protocol.Object, null); await hubConnection.StartAsync(); - connection.Verify(c => c.StartAsync(), Times.Once()); + connection.Verify(c => c.StartAsync(TransferFormat.Text), Times.Once()); } [Fact] @@ -34,7 +37,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests { var connection = new Mock(); connection.Setup(m => m.Features).Returns(new FeatureCollection()); - connection.Setup(m => m.StartAsync()).Verifiable(); + connection.Setup(m => m.StartAsync(TransferFormat.Text)).Verifiable(); var hubConnection = new HubConnection(connection.Object, Mock.Of(), null); await hubConnection.DisposeAsync(); @@ -249,7 +252,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests public string Name => "MockHubProtocol"; - public ProtocolType Type => ProtocolType.Binary; + public TransferFormat TransferFormat => TransferFormat.Binary; public bool TryParseMessages(ReadOnlySpan input, IInvocationBinder binder, IList messages) { diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/LongPollingTransportTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/LongPollingTransportTests.cs index fafe9c1945..a40aaecdba 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/LongPollingTransportTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/LongPollingTransportTests.cs @@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Client.Tests try { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection()); + await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection()); transportActiveTask = longPollingTransport.Running; @@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Client.Tests try { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection()); + await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection()); await longPollingTransport.Running.OrTimeout(); @@ -134,7 +134,7 @@ namespace Microsoft.AspNetCore.Client.Tests try { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection()); + await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection()); var data = await pair.Transport.Input.ReadAllAsync().OrTimeout(); await longPollingTransport.Running.OrTimeout(); @@ -165,7 +165,7 @@ namespace Microsoft.AspNetCore.Client.Tests try { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection()); + await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection()); var exception = await Assert.ThrowsAsync(async () => @@ -207,7 +207,7 @@ namespace Microsoft.AspNetCore.Client.Tests try { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection()); + await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection()); await pair.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello World")); @@ -241,7 +241,7 @@ namespace Microsoft.AspNetCore.Client.Tests try { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection()); + await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection()); pair.Transport.Output.Complete(); @@ -289,7 +289,7 @@ namespace Microsoft.AspNetCore.Client.Tests var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); // Start the transport - await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection()); + await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection()); // Wait for the transport to finish await longPollingTransport.Running.OrTimeout(); @@ -341,7 +341,7 @@ namespace Microsoft.AspNetCore.Client.Tests await pair.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("World")); // Start the transport - await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection()); + await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection()); pair.Transport.Output.Complete(); @@ -360,9 +360,9 @@ namespace Microsoft.AspNetCore.Client.Tests } [Theory] - [InlineData(TransferMode.Binary)] - [InlineData(TransferMode.Text)] - public async Task LongPollingTransportSetsTransferMode(TransferMode transferMode) + [InlineData(TransferFormat.Binary)] + [InlineData(TransferFormat.Text)] + public async Task LongPollingTransportSetsTransferFormat(TransferFormat transferFormat) { var mockHttpHandler = new Mock(); mockHttpHandler.Protected() @@ -381,9 +381,7 @@ namespace Microsoft.AspNetCore.Client.Tests { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - Assert.Null(longPollingTransport.Mode); - await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, transferMode, connection: new TestConnection()); - Assert.Equal(transferMode, longPollingTransport.Mode); + await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, transferFormat, connection: new TestConnection()); } finally { @@ -415,8 +413,7 @@ namespace Microsoft.AspNetCore.Client.Tests { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - Assert.Null(longPollingTransport.Mode); - await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: new TestConnection()); + await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: new TestConnection()); } finally { @@ -436,8 +433,10 @@ namespace Microsoft.AspNetCore.Client.Tests Assert.Equal(assemblyVersion.InformationalVersion, userAgentHeader.Product.Version); } - [Fact] - public async Task LongPollingTransportThrowsForInvalidTransferMode() + [Theory] + [InlineData(TransferFormat.Text | TransferFormat.Binary)] // Multiple values not allowed + [InlineData((TransferFormat)42)] // Unexpected value + public async Task LongPollingTransportThrowsForInvalidTransferFormat(TransferFormat transferFormat) { var mockHttpHandler = new Mock(); mockHttpHandler.Protected() @@ -452,10 +451,10 @@ namespace Microsoft.AspNetCore.Client.Tests { var longPollingTransport = new LongPollingTransport(httpClient); var exception = await Assert.ThrowsAsync(() => - longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), null, TransferMode.Text | TransferMode.Binary, connection: new TestConnection())); + longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), null, transferFormat, connection: new TestConnection())); - Assert.Contains("Invalid transfer mode.", exception.Message); - Assert.Equal("requestedTransferMode", exception.ParamName); + Assert.Contains($"The '{transferFormat}' transfer format is not supported by this transport.", exception.Message); + Assert.Equal("transferFormat", exception.ParamName); } } @@ -488,7 +487,7 @@ namespace Microsoft.AspNetCore.Client.Tests try { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Binary, connection: new TestConnection()); + await longPollingTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: new TestConnection()); var completedTask = await Task.WhenAny(completionTcs.Task, longPollingTransport.Running).OrTimeout(); Assert.Equal(completionTcs.Task, completedTask); diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/ResponseUtils.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/ResponseUtils.cs index 32b457173d..b24c6ab104 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/ResponseUtils.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/ResponseUtils.cs @@ -2,10 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Net; using System.Net.Http; -using System.Text; - +using Microsoft.AspNetCore.Sockets; +using Newtonsoft.Json; using SocketsTransportType = Microsoft.AspNetCore.Sockets.TransportType; namespace Microsoft.AspNetCore.Client.Tests @@ -36,35 +37,36 @@ namespace Microsoft.AspNetCore.Client.Tests } public static string CreateNegotiationContent(string connectionId = "00000000-0000-0000-0000-000000000000", - SocketsTransportType? transportTypes = SocketsTransportType.All) + SocketsTransportType transportTypes = SocketsTransportType.All) { - var sb = new StringBuilder("{ "); - if (connectionId != null) - { - sb.Append($"\"connectionId\": \"{connectionId}\","); - } - if (transportTypes != null) - { - sb.Append($"\"availableTransports\": [ "); - if ((transportTypes & SocketsTransportType.WebSockets) == SocketsTransportType.WebSockets) - { - sb.Append($"\"{nameof(SocketsTransportType.WebSockets)}\","); - } - if ((transportTypes & SocketsTransportType.ServerSentEvents) == SocketsTransportType.ServerSentEvents) - { - sb.Append($"\"{nameof(SocketsTransportType.ServerSentEvents)}\","); - } - if ((transportTypes & SocketsTransportType.LongPolling) == SocketsTransportType.LongPolling) - { - sb.Append($"\"{nameof(SocketsTransportType.LongPolling)}\","); - } - sb.Length--; - sb.Append("],"); - } - sb.Length--; - sb.Append("}"); + var availableTransports = new List(); - return sb.ToString(); + if ((transportTypes & SocketsTransportType.WebSockets) != 0) + { + availableTransports.Add(new + { + transport = nameof(SocketsTransportType.WebSockets), + transferFormats = new[] { nameof(TransferFormat.Text), nameof(TransferFormat.Binary) } + }); + } + if ((transportTypes & SocketsTransportType.ServerSentEvents) != 0) + { + availableTransports.Add(new + { + transport = nameof(SocketsTransportType.ServerSentEvents), + transferFormats = new[] { nameof(TransferFormat.Text) } + }); + } + if ((transportTypes & SocketsTransportType.LongPolling) != 0) + { + availableTransports.Add(new + { + transport = nameof(SocketsTransportType.LongPolling), + transferFormats = new[] { nameof(TransferFormat.Text), nameof(TransferFormat.Binary) } + }); + } + + return JsonConvert.SerializeObject(new { connectionId, availableTransports }); } } } diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/ServerSentEventsTransportTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/ServerSentEventsTransportTests.cs index 0bd8dc9ee5..ce8e0d012b 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/ServerSentEventsTransportTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/ServerSentEventsTransportTests.cs @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var sseTransport = new ServerSentEventsTransport(httpClient); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); await sseTransport.StartAsync( - new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of()).OrTimeout(); + new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of()).OrTimeout(); await eventStreamTcs.Task.OrTimeout(); await sseTransport.StopAsync().OrTimeout(); @@ -105,7 +105,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); await sseTransport.StartAsync( - new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of()).OrTimeout(); + new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of()).OrTimeout(); transportActiveTask = sseTransport.Running; Assert.False(transportActiveTask.IsCompleted); @@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); await sseTransport.StartAsync( - new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of()).OrTimeout(); + new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of()).OrTimeout(); var exception = await Assert.ThrowsAsync(() => sseTransport.Running.OrTimeout()); Assert.Equal("Incomplete message.", exception.Message); @@ -195,7 +195,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); await sseTransport.StartAsync( - new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of()).OrTimeout(); + new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of()).OrTimeout(); await eventStreamTcs.Task; await pair.Transport.Output.WriteAsync(new byte[] { 0x42 }); @@ -239,7 +239,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); await sseTransport.StartAsync( - new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of()).OrTimeout(); + new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of()).OrTimeout(); await eventStreamTcs.Task.OrTimeout(); pair.Transport.Output.Complete(); @@ -267,7 +267,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); await sseTransport.StartAsync( - new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of()).OrTimeout(); + new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of()).OrTimeout(); var message = await pair.Transport.Input.ReadSingleAsync().OrTimeout(); Assert.Equal("3:abc", Encoding.ASCII.GetString(message)); @@ -276,10 +276,8 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } - [Theory] - [InlineData(TransferMode.Text)] - [InlineData(TransferMode.Binary)] - public async Task SSETransportSetsTransferMode(TransferMode transferMode) + [Fact] + public async Task SSETransportDoesNotSupportBinary() { var mockHttpHandler = new Mock(); mockHttpHandler.Protected() @@ -293,12 +291,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests using (var httpClient = new HttpClient(mockHttpHandler.Object)) { var sseTransport = new ServerSentEventsTransport(httpClient); - Assert.Null(sseTransport.Mode); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - await sseTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, transferMode, connection: Mock.Of()).OrTimeout(); - Assert.Equal(TransferMode.Text, sseTransport.Mode); - await sseTransport.StopAsync().OrTimeout(); + var ex = await Assert.ThrowsAsync(() => sseTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Binary, connection: Mock.Of()).OrTimeout()); + Assert.Equal($"The 'Binary' transfer format is not supported by this transport.{Environment.NewLine}Parameter name: transferFormat", ex.Message); } } @@ -322,7 +318,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var sseTransport = new ServerSentEventsTransport(httpClient); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - await sseTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text, connection: Mock.Of()).OrTimeout(); + await sseTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferFormat.Text, connection: Mock.Of()).OrTimeout(); await sseTransport.StopAsync().OrTimeout(); } @@ -338,8 +334,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests Assert.Equal(assemblyVersion.InformationalVersion, userAgentHeader.Product.Version); } - [Fact] - public async Task SSETransportThrowsForInvalidTransferMode() + [Theory] + [InlineData(TransferFormat.Binary)] // Binary not supported + [InlineData(TransferFormat.Text | TransferFormat.Binary)] // Multiple values not allowed + [InlineData((TransferFormat)42)] // Unexpected value + public async Task SSETransportThrowsForInvalidTransferFormat(TransferFormat transferFormat) { var mockHttpHandler = new Mock(); mockHttpHandler.Protected() @@ -354,10 +353,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests { var sseTransport = new ServerSentEventsTransport(httpClient); var exception = await Assert.ThrowsAsync(() => - sseTransport.StartAsync(new Uri("http://fakeuri.org"), null, TransferMode.Text | TransferMode.Binary, connection: Mock.Of())); + sseTransport.StartAsync(new Uri("http://fakeuri.org"), null, transferFormat, connection: Mock.Of())); - Assert.Contains("Invalid transfer mode.", exception.Message); - Assert.Equal("requestedTransferMode", exception.ParamName); + Assert.Contains($"The '{transferFormat}' transfer format is not supported by this transport.", exception.Message); + Assert.Equal("transferFormat", exception.ParamName); } } } diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs index dcd71f92d2..3caf22d4ec 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs @@ -29,8 +29,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests private CancellationTokenSource _receiveShutdownToken = new CancellationTokenSource(); private Task _receiveLoop; - private TransferMode? _transferMode; - public event Action Closed; public Task Started => _started.Task; public Task Disposed => _disposed.Task; @@ -44,9 +42,8 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests public IFeatureCollection Features { get; } = new FeatureCollection(); - public TestConnection(TransferMode? transferMode = null) + public TestConnection() { - _transferMode = transferMode; _receiveLoop = ReceiveLoopAsync(_receiveShutdownToken.Token); } @@ -80,20 +77,8 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests throw new ObjectDisposedException("Unable to send message, underlying channel was closed"); } - public Task StartAsync() + public Task StartAsync(TransferFormat transferFormat) { - if (_transferMode.HasValue) - { - var transferModeFeature = Features.Get(); - if (transferModeFeature == null) - { - transferModeFeature = new TransferModeFeature(); - Features.Set(transferModeFeature); - } - - transferModeFeature.TransferMode = _transferMode.Value; - } - _started.TrySetResult(null); return Task.CompletedTask; } diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestTransport.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestTransport.cs index ddbb37bf82..e5cc1d01fe 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestTransport.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestTransport.cs @@ -1,6 +1,5 @@ using System; using System.IO.Pipelines; -using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets.Client; @@ -12,18 +11,22 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests private readonly Func _stopHandler; private readonly Func _startHandler; - public TransferMode? Mode { get; } + public TransferFormat? Format { get; } public IDuplexPipe Application { get; private set; } - public TestTransport(Func onTransportStop = null, Func onTransportStart = null, TransferMode transferMode = TransferMode.Text) + public TestTransport(Func onTransportStop = null, Func onTransportStart = null, TransferFormat transferFormat = TransferFormat.Text) { _stopHandler = onTransportStop ?? new Func(() => Task.CompletedTask); _startHandler = onTransportStart ?? new Func(() => Task.CompletedTask); - Mode = transferMode; + Format = transferFormat; } - public Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection) + public Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection) { + if ((Format & transferFormat) == 0) + { + throw new InvalidOperationException($"The '{transferFormat}' transfer format is not supported by this transport."); + } Application = application; return _startHandler(); } diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Encoders/Base64EncoderTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Encoders/Base64EncoderTests.cs deleted file mode 100644 index 0db0079ae5..0000000000 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Encoders/Base64EncoderTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.Text; -using Xunit; - -namespace Microsoft.AspNetCore.SignalR.Internal.Encoders -{ - public class Base64EncoderTests - { - [Theory] - [MemberData(nameof(Payloads))] - public void VerifyDecode(string payload, string encoded) - { - var message = Encoding.UTF8.GetBytes(payload); - var encodedMessage = Encoding.UTF8.GetString(new Base64Encoder().Encode(message)); - Assert.Equal(encoded, encodedMessage); - } - - [Theory] - [MemberData(nameof(Payloads))] - public void VerifyEncode(string payload, string encoded) - { - ReadOnlySpan encodedMessage = Encoding.UTF8.GetBytes(encoded); - var encoder = new Base64Encoder(); - encoder.TryDecode(ref encodedMessage, out var data); - var decodedMessage = Encoding.UTF8.GetString(data.ToArray()); - Assert.Equal(payload, decodedMessage); - } - - [Fact] - public void CanParseMultipleMessages() - { - ReadOnlySpan data = Encoding.UTF8.GetBytes("28:QQpSDUMNCjtERUYxMjM0NTY3ODkw;4:QUJD;4:QUJD;"); - var encoder = new Base64Encoder(); - Assert.True(encoder.TryDecode(ref data, out var payload1)); - Assert.True(encoder.TryDecode(ref data, out var payload2)); - Assert.True(encoder.TryDecode(ref data, out var payload3)); - Assert.False(encoder.TryDecode(ref data, out var payload4)); - Assert.Equal(0, data.Length); - var payload1Value = Encoding.UTF8.GetString(payload1.ToArray()); - var payload2Value = Encoding.UTF8.GetString(payload2.ToArray()); - var payload3Value = Encoding.UTF8.GetString(payload3.ToArray()); - Assert.Equal("A\nR\rC\r\n;DEF1234567890", payload1Value); - Assert.Equal("ABC", payload2Value); - Assert.Equal("ABC", payload3Value); - } - - public static IEnumerable Payloads => - new object[][] - { - new object[] { "", "0:;" }, - new object[] { "ABC", "4:QUJD;" }, - new object[] { "A\nR\rC\r\n;DEF1234567890", "28:QQpSDUMNCjtERUYxMjM0NTY3ODkw;" }, - }; - } -} diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Encoders/LengthPrefixedTextMessageParserTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Encoders/LengthPrefixedTextMessageParserTests.cs deleted file mode 100644 index eba7ac832d..0000000000 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Encoders/LengthPrefixedTextMessageParserTests.cs +++ /dev/null @@ -1,90 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.AspNetCore.SignalR.Internal.Encoders; -using Xunit; - -namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Encoders -{ - public class LengthPrefixedTextMessageParserTests - { - [Theory] - [InlineData("0:;", "")] - [InlineData("3:ABC;", "ABC")] - [InlineData("11:A\nR\rC\r\n;DEF;", "A\nR\rC\r\n;DEF")] - [InlineData("12:Hello, World;", "Hello, World")] - public void ReadTextMessage(string encoded, string payload) - { - ReadOnlySpan buffer = Encoding.UTF8.GetBytes(encoded); - - Assert.True(LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out var message)); - Assert.Equal(0, buffer.Length); - Assert.Equal(Encoding.UTF8.GetBytes(payload), message.ToArray()); - } - - [Fact] - public void ReadMultipleMessages() - { - const string encoded = "0:;14:Hello,\r\nWorld!;"; - ReadOnlySpan buffer = Encoding.UTF8.GetBytes(encoded); - - var messages = new List(); - while (LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out var message)) - { - messages.Add(message.ToArray()); - } - - Assert.Equal(0, buffer.Length); - - Assert.Equal(2, messages.Count); - Assert.Equal(new byte[0], messages[0]); - Assert.Equal(Encoding.UTF8.GetBytes("Hello,\r\nWorld!"), messages[1]); - } - - [Theory] - [InlineData("")] - [InlineData("ABC")] - [InlineData("1230450945")] - [InlineData("1:")] - [InlineData("10")] - [InlineData("5:A")] - [InlineData("5:ABCDE")] - public void ReadIncompleteMessages(string encoded) - { - ReadOnlySpan buffer = Encoding.UTF8.GetBytes(encoded); - Assert.False(LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out _)); - } - - [Theory] - [InlineData("X:", "Invalid length: 'X'")] - [InlineData("1:asdf", "Missing delimiter ';' after payload")] - [InlineData("1029348109238412903849023841290834901283409128349018239048102394:ABCDEF", "Invalid length: '1029348109238412903849023841290834901283409128349018239048102394'")] - [InlineData("12ab34:", "Invalid length: '12ab34'")] - [InlineData("5:ABCDEF", "Missing delimiter ';' after payload")] - public void ReadInvalidMessages(string encoded, string expectedMessage) - { - var ex = Assert.Throws(() => - { - ReadOnlySpan buffer = Encoding.UTF8.GetBytes(encoded); - LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out _); - }); - Assert.Equal(expectedMessage, ex.Message); - } - - [Fact] - public void ReadInvalidEncodedMessage() - { - var ex = Assert.Throws(() => - { - // Invalid because first character is a UTF-8 "continuation" character - // We need to include the ':' so that - ReadOnlySpan buffer = new byte[] { 0x48, 0x65, 0x80, 0x6C, 0x6F, (byte)':' }; - LengthPrefixedTextMessageParser.TryParseMessage(ref buffer, out _); - }); - Assert.Equal("Invalid length: 'He�lo'", ex.Message); - } - } -} diff --git a/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisEndToEnd.cs b/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisEndToEnd.cs index 10b2a9bb53..08128bcb95 100644 --- a/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisEndToEnd.cs +++ b/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisEndToEnd.cs @@ -112,7 +112,11 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests foreach (var transport in TransportTypes()) { yield return new object[] { transport, new JsonHubProtocol() }; - yield return new object[] { transport, new MessagePackHubProtocol() }; + + if (transport != TransportType.ServerSentEvents) + { + yield return new object[] { transport, new MessagePackHubProtocol() }; + } } } } diff --git a/test/Microsoft.AspNetCore.SignalR.Tests.Utils/HubConnectionContextUtils.cs b/test/Microsoft.AspNetCore.SignalR.Tests.Utils/HubConnectionContextUtils.cs index 25f6327627..8d1f8dba54 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests.Utils/HubConnectionContextUtils.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests.Utils/HubConnectionContextUtils.cs @@ -3,7 +3,6 @@ using System; using Microsoft.AspNetCore.SignalR.Internal; -using Microsoft.AspNetCore.SignalR.Internal.Encoders; using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.Sockets; using Microsoft.Extensions.Logging.Abstractions; @@ -17,15 +16,15 @@ namespace Microsoft.AspNetCore.SignalR.Tests { return new HubConnectionContext(connection, TimeSpan.FromSeconds(15), NullLoggerFactory.Instance) { - ProtocolReaderWriter = new HubProtocolReaderWriter(new JsonHubProtocol(), new PassThroughEncoder()) + Protocol = new JsonHubProtocol() }; } public static Mock CreateMock(DefaultConnectionContext connection) { var mock = new Mock(connection, TimeSpan.FromSeconds(15), NullLoggerFactory.Instance) { CallBase = true }; - var readerWriter = new HubProtocolReaderWriter(new JsonHubProtocol(), new PassThroughEncoder()); - mock.SetupGet(m => m.ProtocolReaderWriter).Returns(readerWriter); + var protocol = new JsonHubProtocol(); + mock.SetupGet(m => m.Protocol).Returns(protocol); return mock; } diff --git a/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs b/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs index 954ca28534..d0784a39c0 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Buffers; using System.Collections.Generic; using System.IO; using System.IO.Pipelines; @@ -9,7 +10,6 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Internal; -using Microsoft.AspNetCore.SignalR.Internal.Encoders; using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.Sockets; @@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests public class TestClient : IDisposable { private static int _id; - private readonly HubProtocolReaderWriter _protocolReaderWriter; + private readonly IHubProtocol _protocol; private readonly IInvocationBinder _invocationBinder; private CancellationTokenSource _cts; private Queue _messages = new Queue(); @@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests public DefaultConnectionContext Connection { get; } public Task Connected => ((TaskCompletionSource)Connection.Metadata["ConnectedTask"]).Task; - public TestClient(bool synchronousCallbacks = false, IHubProtocol protocol = null, IDataEncoder dataEncoder = null, IInvocationBinder invocationBinder = null, bool addClaimId = false) + public TestClient(bool synchronousCallbacks = false, IHubProtocol protocol = null, IInvocationBinder invocationBinder = null, bool addClaimId = false) { var options = new PipeOptions(readerScheduler: synchronousCallbacks ? PipeScheduler.Inline : null); var pair = DuplexPipe.CreateConnectionPair(options, options); @@ -42,16 +42,14 @@ namespace Microsoft.AspNetCore.SignalR.Tests Connection.User = new ClaimsPrincipal(new ClaimsIdentity(claims)); Connection.Metadata["ConnectedTask"] = new TaskCompletionSource(); - protocol = protocol ?? new JsonHubProtocol(); - dataEncoder = dataEncoder ?? new PassThroughEncoder(); - _protocolReaderWriter = new HubProtocolReaderWriter(protocol, dataEncoder); + _protocol = protocol ?? new JsonHubProtocol(); _invocationBinder = invocationBinder ?? new DefaultInvocationBinder(); _cts = new CancellationTokenSource(); using (var memoryStream = new MemoryStream()) { - NegotiationProtocol.WriteMessage(new NegotiationMessage(protocol.Name), memoryStream); + NegotiationProtocol.WriteMessage(new NegotiationMessage(_protocol.Name), memoryStream); Connection.Application.Output.WriteAsync(memoryStream.ToArray()); } } @@ -143,7 +141,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests public async Task SendHubMessageAsync(HubMessage message) { - var payload = _protocolReaderWriter.WriteMessage(message); + var payload = _protocol.WriteToArray(message); + await Connection.Application.Output.WriteAsync(payload); return message is HubInvocationMessage hubMessage ? hubMessage.InvocationId : null; } @@ -201,7 +200,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests try { - if (_protocolReaderWriter.ReadMessages(result.Buffer, _invocationBinder, out var messages, out consumed, out examined)) + var messages = new List(); + if (_protocol.TryParseMessages(result.Buffer.ToArray(), _invocationBinder, messages)) { foreach (var m in messages) { diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/EndToEndTests.cs b/test/Microsoft.AspNetCore.SignalR.Tests/EndToEndTests.cs index e12ae52925..ee379186fe 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/EndToEndTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests/EndToEndTests.cs @@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests // The test should connect to the server using WebSockets transport on Windows 8 and newer. // On Windows 7/2008R2 it should use ServerSentEvents transport to connect to the server. var connection = new HttpConnection(new Uri(url)); - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Binary).OrTimeout(); await connection.DisposeAsync().OrTimeout(); } @@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests // The test logic lives in the TestTransportFactory and FakeTransport. var connection = new HttpConnection(new Uri(url), new TestTransportFactory(), null, null); - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); await connection.DisposeAsync().OrTimeout(); } @@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { var url = _serverFixture.Url + "/echo"; var connection = new HttpConnection(new Uri(url), transportType); - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Text).OrTimeout(); await connection.DisposeAsync().OrTimeout(); } @@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests }, receiveTcs); var message = new byte[] { 42 }; - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Binary).OrTimeout(); await connection.SendAsync(message).OrTimeout(); var receivedData = await receiveTcs.Task.OrTimeout(); @@ -166,8 +166,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests } [Theory(Skip = "https://github.com/aspnet/SignalR/issues/1485")] - [MemberData(nameof(TransportTypesAndTransferModes))] - public async Task ConnectionCanSendAndReceiveMessages(TransportType transportType, TransferMode requestedTransferMode) + [MemberData(nameof(TransportTypesAndTransferFormats))] + public async Task ConnectionCanSendAndReceiveMessages(TransportType transportType, TransferFormat requestedTransferFormat) { using (StartLog(out var loggerFactory, testName: $"ConnectionCanSendAndReceiveMessages_{transportType.ToString()}")) { @@ -177,9 +177,6 @@ namespace Microsoft.AspNetCore.SignalR.Tests var url = _serverFixture.Url + "/echo"; var connection = new HttpConnection(new Uri(url), transportType, loggerFactory); - - connection.Features.Set( - new TransferModeFeature { TransferMode = requestedTransferMode }); try { var closeTcs = new TaskCompletionSource(); @@ -199,28 +196,17 @@ namespace Microsoft.AspNetCore.SignalR.Tests connection.OnReceived((data, state) => { logger.LogInformation("Received {length} byte message", data.Length); - - if (IsBase64Encoded(requestedTransferMode, connection)) - { - data = Convert.FromBase64String(Encoding.UTF8.GetString(data)); - } var tcs = (TaskCompletionSource)state; tcs.TrySetResult(Encoding.UTF8.GetString(data)); return Task.CompletedTask; }, receiveTcs); logger.LogInformation("Starting connection to {url}", url); - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(requestedTransferFormat).OrTimeout(); logger.LogInformation("Started connection to {url}", url); var bytes = Encoding.UTF8.GetBytes(message); - // Need to encode binary payloads sent over text transports - if (IsBase64Encoded(requestedTransferMode, connection)) - { - bytes = Encoding.UTF8.GetBytes(Convert.ToBase64String(bytes)); - } - logger.LogInformation("Sending {length} byte message", bytes.Length); try { @@ -255,12 +241,6 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } - private bool IsBase64Encoded(TransferMode transferMode, IConnection connection) - { - return transferMode == TransferMode.Binary && - connection.Features.Get().TransferMode == TransferMode.Text; - } - public static IEnumerable MessageSizesData { get @@ -281,8 +261,6 @@ namespace Microsoft.AspNetCore.SignalR.Tests var url = _serverFixture.Url + "/echo"; var connection = new HttpConnection(new Uri(url), TransportType.WebSockets, loggerFactory); - connection.Features.Set( - new TransferModeFeature { TransferMode = TransferMode.Binary }); try { @@ -296,7 +274,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests }, receiveTcs); logger.LogInformation("Starting connection to {url}", url); - await connection.StartAsync().OrTimeout(); + await connection.StartAsync(TransferFormat.Binary).OrTimeout(); logger.LogInformation("Started connection to {url}", url); var bytes = Encoding.UTF8.GetBytes(message); @@ -416,16 +394,15 @@ namespace Microsoft.AspNetCore.SignalR.Tests private class FakeTransport : ITransport { - public TransferMode? Mode => TransferMode.Text; public string prevConnectionId = null; private int tries = 0; private IDuplexPipe _application; - public Task StartAsync(Uri url, IDuplexPipe application, TransferMode requestedTransferMode, IConnection connection) + public Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection) { _application = application; tries++; - Assert.True(QueryHelpers.ParseQuery(url.Query.ToString()).TryGetValue("id", out var id)); + Assert.True(QueryHelpers.ParseQuery(url.Query.ToString()).TryGetValue("id", out var id)); if (prevConnectionId == null) { prevConnectionId = id; @@ -467,14 +444,18 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } - public static IEnumerable TransportTypesAndTransferModes + public static IEnumerable TransportTypesAndTransferFormats { get { foreach (var transport in TransportTypes) { - yield return new object[] { transport[0], TransferMode.Text }; - yield return new object[] { transport[0], TransferMode.Binary }; + yield return new object[] { transport[0], TransferFormat.Text }; + + if ((TransportType)transport[0] != TransportType.ServerSentEvents) + { + yield return new object[] { transport[0], TransferFormat.Binary }; + } } } } diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs b/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs index 2ef56dbc0d..3901499ec4 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests/HubEndpointTests.cs @@ -9,7 +9,6 @@ using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR.Internal; -using Microsoft.AspNetCore.SignalR.Internal.Encoders; using Microsoft.AspNetCore.SignalR.Internal.Protocol; using Microsoft.AspNetCore.SignalR.Tests.HubEndpointTestUtils; using Microsoft.AspNetCore.Sockets; @@ -1333,10 +1332,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests using (var client = new TestClient(synchronousCallbacks: false, protocol: protocol, invocationBinder: invocationBinder.Object)) { - var transportFeature = new Mock(); - transportFeature.SetupGet(f => f.TransportCapabilities) - .Returns(protocol.Type == ProtocolType.Binary ? TransferMode.Binary : TransferMode.Text); - client.Connection.Features.Set(transportFeature.Object); + client.Connection.SupportedFormats = protocol.TransferFormat; var endPointLifetime = endPoint.OnConnectedAsync(client.Connection); @@ -1419,7 +1415,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests var endPoint = serviceProvider.GetService>(); using (var client1 = new TestClient(protocol: new JsonHubProtocol())) - using (var client2 = new TestClient(protocol: new MessagePackHubProtocol(), dataEncoder: new Base64Encoder())) + using (var client2 = new TestClient(protocol: new MessagePackHubProtocol())) { var endPointLifetime1 = endPoint.OnConnectedAsync(client1.Connection); var endPointLifetime2 = endPoint.OnConnectedAsync(client2.Connection); @@ -1617,9 +1613,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests var msgPackOptions = serviceProvider.GetRequiredService>(); using (var client = new TestClient(synchronousCallbacks: false, protocol: new MessagePackHubProtocol(msgPackOptions))) { - var transportFeature = new Mock(); - transportFeature.SetupGet(f => f.TransportCapabilities).Returns(TransferMode.Binary); - client.Connection.Features.Set(transportFeature.Object); + client.Connection.SupportedFormats = TransferFormat.Binary; var endPointLifetime = endPoint.OnConnectedAsync(client.Connection); await client.Connected.OrTimeout(); @@ -1792,6 +1786,21 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } + [Fact] + public async Task NegotiatingFailsIfMsgPackRequestedOverTextOnlyTransport() + { + var serviceProvider = HubEndPointTestUtils.CreateServiceProvider(services => + services.Configure(options => + options.KeepAliveInterval = TimeSpan.FromMilliseconds(100))); + var endPoint = serviceProvider.GetService>(); + + using (var client = new TestClient(false, new MessagePackHubProtocol())) + { + client.Connection.SupportedFormats = TransferFormat.Text; + var ex = await Assert.ThrowsAsync(() => endPoint.OnConnectedAsync(client.Connection).OrTimeout()); + } + } + public static IEnumerable HubTypes() { yield return new[] { typeof(DynamicTestHub) }; diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/WebSocketsTransportTests.cs b/test/Microsoft.AspNetCore.SignalR.Tests/WebSocketsTransportTests.cs index 0b3d93b011..7b39ba6c8e 100644 --- a/test/Microsoft.AspNetCore.SignalR.Tests/WebSocketsTransportTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Tests/WebSocketsTransportTests.cs @@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application, - TransferMode.Binary, connection: Mock.Of()).OrTimeout(); + TransferFormat.Binary, connection: Mock.Of()).OrTimeout(); await webSocketsTransport.StopAsync().OrTimeout(); await webSocketsTransport.Running.OrTimeout(); } @@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/httpheader"), pair.Application, - TransferMode.Binary, connection: Mock.Of()).OrTimeout(); + TransferFormat.Binary, connection: Mock.Of()).OrTimeout(); await pair.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes("User-Agent")); @@ -120,7 +120,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application, - TransferMode.Binary, connection: Mock.Of()); + TransferFormat.Binary, connection: Mock.Of()); pair.Transport.Output.Complete(); await webSocketsTransport.Running.OrTimeout(TimeSpan.FromSeconds(10)); } @@ -128,15 +128,15 @@ namespace Microsoft.AspNetCore.SignalR.Tests [ConditionalTheory] [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")] - [InlineData(TransferMode.Text)] - [InlineData(TransferMode.Binary)] - public async Task WebSocketsTransportStopsWhenConnectionClosedByTheServer(TransferMode transferMode) + [InlineData(TransferFormat.Text)] + [InlineData(TransferFormat.Binary)] + public async Task WebSocketsTransportStopsWhenConnectionClosedByTheServer(TransferFormat transferFormat) { using (StartLog(out var loggerFactory)) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); - await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application, transferMode, connection: Mock.Of()); + await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application, transferFormat, connection: Mock.Of()); await pair.Transport.Output.WriteAsync(new byte[] { 0x42 }); @@ -151,38 +151,38 @@ namespace Microsoft.AspNetCore.SignalR.Tests [ConditionalTheory] [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")] - [InlineData(TransferMode.Text)] - [InlineData(TransferMode.Binary)] - public async Task WebSocketsTransportSetsTransferMode(TransferMode transferMode) + [InlineData(TransferFormat.Text)] + [InlineData(TransferFormat.Binary)] + public async Task WebSocketsTransportSetsTransferFormat(TransferFormat transferFormat) { using (StartLog(out var loggerFactory)) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); - Assert.Null(webSocketsTransport.Mode); await webSocketsTransport.StartAsync(new Uri(_serverFixture.WebSocketsUrl + "/echo"), pair.Application, - transferMode, connection: Mock.Of()).OrTimeout(); - Assert.Equal(transferMode, webSocketsTransport.Mode); + transferFormat, connection: Mock.Of()).OrTimeout(); await webSocketsTransport.StopAsync().OrTimeout(); await webSocketsTransport.Running.OrTimeout(); } } - [ConditionalFact] + [ConditionalTheory] + [InlineData(TransferFormat.Text | TransferFormat.Binary)] // Multiple values not allowed + [InlineData((TransferFormat)42)] // Unexpected value [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2, SkipReason = "No WebSockets Client for this platform")] - public async Task WebSocketsTransportThrowsForInvalidTransferMode() + public async Task WebSocketsTransportThrowsForInvalidTransferFormat(TransferFormat transferFormat) { using (StartLog(out var loggerFactory)) { var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var webSocketsTransport = new WebSocketsTransport(httpOptions: null, loggerFactory: loggerFactory); var exception = await Assert.ThrowsAsync(() => - webSocketsTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, TransferMode.Text | TransferMode.Binary, connection: Mock.Of())); + webSocketsTransport.StartAsync(new Uri("http://fakeuri.org"), pair.Application, transferFormat, connection: Mock.Of())); - Assert.Contains("Invalid transfer mode.", exception.Message); - Assert.Equal("requestedTransferMode", exception.ParamName); + Assert.Contains($"The '{transferFormat}' transfer format is not supported by this transport.", exception.Message); + Assert.Equal("transferFormat", exception.ParamName); } } } diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/HttpConnectionDispatcherTests.cs b/test/Microsoft.AspNetCore.Sockets.Tests/HttpConnectionDispatcherTests.cs index 04b43d08df..ddc47335ca 100644 --- a/test/Microsoft.AspNetCore.Sockets.Tests/HttpConnectionDispatcherTests.cs +++ b/test/Microsoft.AspNetCore.Sockets.Tests/HttpConnectionDispatcherTests.cs @@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests [InlineData(TransportType.All)] [InlineData((TransportType)0)] [InlineData(TransportType.LongPolling | TransportType.WebSockets)] - public async Task NegotiateReturnsAvailableTransports(TransportType transports) + public async Task NegotiateReturnsAvailableTransportsAfterFilteringByOptions(TransportType transports) { using (StartLog(out var loggerFactory, LogLevel.Debug)) { @@ -167,7 +167,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests var availableTransports = (TransportType)0; foreach (var transport in negotiateResponse["availableTransports"]) { - availableTransports |= (TransportType)Enum.Parse(typeof(TransportType), transport.Value()); + var transportType = (TransportType)Enum.Parse(typeof(TransportType), transport.Value("transport")); + availableTransports |= transportType; } Assert.Equal(transports, availableTransports); @@ -820,10 +821,10 @@ namespace Microsoft.AspNetCore.Sockets.Tests } [Theory] - [InlineData(TransportType.LongPolling, TransferMode.Binary | TransferMode.Text)] - [InlineData(TransportType.ServerSentEvents, TransferMode.Text)] - [InlineData(TransportType.WebSockets, TransferMode.Binary | TransferMode.Text)] - public async Task TransportCapabilitiesSet(TransportType transportType, TransferMode expectedTransportCapabilities) + [InlineData(TransportType.LongPolling, null)] + [InlineData(TransportType.ServerSentEvents, TransferFormat.Text)] + [InlineData(TransportType.WebSockets, TransferFormat.Binary | TransferFormat.Text)] + public async Task TransferModeSet(TransportType transportType, TransferFormat? expectedTransferFormats) { using (StartLog(out var loggerFactory, LogLevel.Debug)) { @@ -845,7 +846,11 @@ namespace Microsoft.AspNetCore.Sockets.Tests options.WebSockets.CloseTimeout = TimeSpan.FromSeconds(0); await dispatcher.ExecuteAsync(context, options, app); - Assert.Equal(expectedTransportCapabilities, connection.TransportCapabilities); + if (expectedTransferFormats != null) + { + var transferFormatFeature = connection.Features.Get(); + Assert.Equal(expectedTransferFormats.Value, transferFormatFeature.SupportedFormats); + } } } diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/WebSocketsTests.cs b/test/Microsoft.AspNetCore.Sockets.Tests/WebSocketsTests.cs index a9b806fc93..35961acb85 100644 --- a/test/Microsoft.AspNetCore.Sockets.Tests/WebSocketsTests.cs +++ b/test/Microsoft.AspNetCore.Sockets.Tests/WebSocketsTests.cs @@ -8,6 +8,8 @@ using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Sockets.Features; +using Microsoft.AspNetCore.Sockets.Internal; using Microsoft.AspNetCore.Sockets.Internal.Transports; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; @@ -71,9 +73,9 @@ namespace Microsoft.AspNetCore.Sockets.Tests } [Theory] - [InlineData(TransferMode.Text, WebSocketMessageType.Text)] - [InlineData(TransferMode.Binary, WebSocketMessageType.Binary)] - public async Task WebSocketTransportSetsMessageTypeBasedOnTransferModeFeature(TransferMode transferMode, WebSocketMessageType expectedMessageType) + [InlineData(TransferFormat.Text, WebSocketMessageType.Text)] + [InlineData(TransferFormat.Binary, WebSocketMessageType.Binary)] + public async Task WebSocketTransportSetsMessageTypeBasedOnTransferFormatFeature(TransferFormat transferFormat, WebSocketMessageType expectedMessageType) { using (StartLog(out var loggerFactory, LogLevel.Debug)) { @@ -82,7 +84,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests using (var feature = new TestWebSocketConnectionFeature()) { - var connectionContext = new DefaultConnectionContext(string.Empty, null, null) { TransferMode = transferMode }; + var connectionContext = new DefaultConnectionContext(string.Empty, null, null); + connectionContext.ActiveFormat = transferFormat; var ws = new WebSocketsTransport(new WebSocketOptions(), connection.Application, connectionContext, loggerFactory); // Give the server socket to the transport and run it