From 4f4fb174ea03f3a16821406b613f5d5383342fe1 Mon Sep 17 00:00:00 2001 From: Pawel Kadluczka Date: Fri, 22 Sep 2017 17:51:25 -0700 Subject: [PATCH 1/2] Replacing 8-byte-long length prefix with varint --- .../Formatters.spec.ts | 44 +++++++---- .../MessagePackHubProtocol.spec.ts | 40 ++++------ .../Formatters.ts | 74 +++++++++++------- .../Formatters/BinaryMessageFormatter.cs | 38 ++++++--- .../Formatters/BinaryMessageParser.cs | 52 +++++++++---- ...Microsoft.AspNetCore.SignalR.Common.csproj | 1 + .../Formatters/BinaryMessageFormatterTests.cs | 77 ++++++++++++++++--- .../Formatters/BinaryMessageParserTests.cs | 61 +++++++++++++-- .../Protocol/MessagePackHubProtocolTests.cs | 27 +++---- 9 files changed, 292 insertions(+), 122 deletions(-) diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Formatters.spec.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Formatters.spec.ts index e0ab806c86..947fe1c8f7 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Formatters.spec.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Formatters.spec.ts @@ -29,10 +29,11 @@ describe("Text Message Formatter", () => { describe("Binary Message Formatter", () => { ([ - [[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], [ new Uint8Array([])]], - [[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff], [ new Uint8Array([0xff])]], - [[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x7f], [ new Uint8Array([0xff]), new Uint8Array([0x7f])]], + [[], []], + [[0x00], [ new Uint8Array([])]], + [[0x01, 0xff], [ new Uint8Array([0xff])]], + [[0x01, 0xff, + 0x01, 0x7f], [ new Uint8Array([0xff]), new Uint8Array([0x7f])]], ] as [[number[], Uint8Array[]]]).forEach(([payload, expected_messages]) => { it(`should parse '${payload}' correctly`, () => { let messages = BinaryMessageFormat.parse(new Uint8Array(payload).buffer); @@ -41,14 +42,15 @@ describe("Binary Message Formatter", () => { }); ([ - [[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], new Error("Cannot read message size")], - [[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x80, 0x00], new Error("Cannot read message size")], - [[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], new Error("Messages bigger than 2147483647 bytes are not supported")], - [[0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], new Error("Messages bigger than 2147483647 bytes are not supported")], - [[0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00], new Error("Messages bigger than 2147483647 bytes are not supported")], - [[0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00], new Error("Messages bigger than 2147483647 bytes are not supported")], - [[0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00], new Error("Messages bigger than 2147483647 bytes are not supported")], - [[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00], new Error("Incomplete message")], + [[0x80], new Error("Cannot read message size.")], + [[0x02, 0x01, 0x80, 0x80], new Error("Cannot read message size.")], + [[0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80], new Error("Cannot read message size.")], // the size of the second message is cut + [[0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01], new Error("Incomplete message.")], // second message has only size + [[0xff, 0xff, 0xff, 0xff, 0xff], new Error("Messages bigger than 2GB are not supported.")], + [[0x80, 0x80, 0x80, 0x80, 0x08], new Error("Messages bigger than 2GB are not supported.")], + [[0x80, 0x80, 0x80, 0x80, 0x80], new Error("Messages bigger than 2GB are not supported.")], + [[0x02, 0x00], new Error("Incomplete message.")], + [[0xff, 0xff, 0xff, 0xff, 0x07], new Error("Incomplete message.")] ] as [[number[], Error]]).forEach(([payload, expected_error]) => { it(`should fail to parse '${payload}'`, () => { expect(() => BinaryMessageFormat.parse(new Uint8Array(payload).buffer)).toThrow(expected_error); @@ -56,8 +58,8 @@ describe("Binary Message Formatter", () => { }); ([ - [[], [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]], - [[0x20], [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x20]], + [[], [0x00]], + [[0x20], [0x01, 0x20]], ] as [[number[], number[]]]).forEach(([input, expected_payload]) => { it(`should write '${input}'`, () => { let actual = new Uint8Array(BinaryMessageFormat.write(new Uint8Array(input))); @@ -65,4 +67,18 @@ describe("Binary Message Formatter", () => { expect(actual).toEqual(expected); }) }); + + ([0x0000, 0x0001, 0x007f, 0x0080, 0x3fff, 0x4000, 0xc0de] as number[]).forEach(size => { + it(`messages should be roundtrippable (message size: '${size}')`, () => { + const message = []; + for (let i = 0; i < size; i++) { + message.push(i & 0xff); + } + + var payload = new Uint8Array(message); + expect(payload).toEqual(BinaryMessageFormat.parse(BinaryMessageFormat.write(payload))[0]); + }) + }); + + }); diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts index 5b5e373082..8c908f33d6 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/MessagePackHubProtocol.spec.ts @@ -20,24 +20,21 @@ describe("MessageHubProtocol", () => { }); ([ - [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, - 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x01, 0xa3, 0x45, 0x72, 0x72], + [ [ 0x0b, 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x01, 0xa3, 0x45, 0x72, 0x72], { type: MessageType.Completion, invocationId: "abc", error: "Err", result: null } as CompletionMessage ], - [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, - 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b ], + [ [ 0x0a, 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b ], { type: MessageType.Completion, invocationId: "abc", error: null, result: "OK" } as CompletionMessage ], - [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, - 0x93, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x02 ], + [ [ 0x07, 0x93, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x02 ], { type: MessageType.Completion, invocationId: "abc", @@ -51,8 +48,7 @@ describe("MessageHubProtocol", () => { })); ([ - [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, - 0x93, 0x02, 0xa3, 0x61, 0x62, 0x63, 0x08 ], + [ [ 0x07, 0x93, 0x02, 0xa3, 0x61, 0x62, 0x63, 0x08 ], { type: MessageType.Result, invocationId: "abc", @@ -65,17 +61,17 @@ describe("MessageHubProtocol", () => { })); ([ - [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], new Error("Invalid payload.") ], - [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x90 ], new Error("Invalid payload.") ], - [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc2 ], new Error("Invalid payload.") ], - [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x91, 0x05 ], new Error("Invalid message type.") ], - [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x91, 0xa1, 0x78 ], new Error("Invalid message type.") ], - [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x91, 0x01 ], new Error("Invalid payload for Invocation message.") ], - [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x91, 0x02 ], new Error("Invalid payload for stream Result message.") ], - [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x92, 0x03, 0xa0 ], new Error("Invalid payload for Completion message.") ], - [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x94, 0x03, 0xa0, 0x02, 0x00 ], new Error("Invalid payload for Completion message.") ], - [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x93, 0x03, 0xa0, 0x01 ], new Error("Invalid payload for Completion message.") ], - [ [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x93, 0x03, 0xa0, 0x03 ], new Error("Invalid payload for Completion message.") ] + [ [ 0x00 ], new Error("Invalid payload.") ], + [ [ 0x01, 0x90 ], new Error("Invalid payload.") ], + [ [ 0x01, 0xc2 ], new Error("Invalid payload.") ], + [ [ 0x02, 0x91, 0x05 ], new Error("Invalid message type.") ], + [ [ 0x03, 0x91, 0xa1, 0x78 ], new Error("Invalid message type.") ], + [ [ 0x02, 0x91, 0x01 ], new Error("Invalid payload for Invocation message.") ], + [ [ 0x02, 0x91, 0x02 ], new Error("Invalid payload for stream Result message.") ], + [ [ 0x03, 0x92, 0x03, 0xa0 ], new Error("Invalid payload for Completion message.") ], + [ [ 0x05, 0x94, 0x03, 0xa0, 0x02, 0x00 ], new Error("Invalid payload for Completion message.") ], + [ [ 0x04, 0x93, 0x03, 0xa0, 0x01 ], new Error("Invalid payload for Completion message.") ], + [ [ 0x04, 0x93, 0x03, 0xa0, 0x03 ], new Error("Invalid payload for Completion message.") ] ] as [[number[], Error]]).forEach(([payload, expected_error]) => it("throws for invalid messages", () => { expect(() => new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer)) @@ -84,10 +80,8 @@ describe("MessageHubProtocol", () => { it("can read multiple messages", () => { let payload = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, - 0x93, 0x02, 0xa3, 0x61, 0x62, 0x63, 0x08, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, - 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b ]; + 0x07, 0x93, 0x02, 0xa3, 0x61, 0x62, 0x63, 0x08, + 0x0a, 0x94, 0x03, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b ]; let messages = new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer); expect(messages).toEqual([ { diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Formatters.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Formatters.ts index d4edd4db41..57adac52ff 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Formatters.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Formatters.ts @@ -21,55 +21,73 @@ export namespace TextMessageFormat { } export namespace BinaryMessageFormat { + + // The length prefix of binary messages is encoded as VarInt. Read the comment in + // the BinaryMessageParser.TryParseMessage for details. + export function write(output: Uint8Array): ArrayBuffer { - // .byteLength does is undefined in IE10 + // msgpack5 uses returns Buffer instead of Uint8Array on IE10 and some other browser + // in which case .byteLength does will be undefined let size = output.byteLength || output.length; - let buffer = new Uint8Array(size + 8); - - // javascript bitwise operators only support 32-bit integers - for (let i = 7; i >= 4; i--) { - buffer[i] = size & 0xff; - size = size >> 8; + let lenBuffer = []; + do + { + let sizePart = size & 0x7f; + size = size >> 7; + if (size > 0) { + sizePart |= 0x80; + } + lenBuffer.push(sizePart); } + while (size > 0); - buffer.set(output, 8); + // msgpack5 uses returns Buffer instead of Uint8Array on IE10 and some other browser + // in which case .byteLength does will be undefined + size = output.byteLength || output.length; + let buffer = new Uint8Array(lenBuffer.length + size); + buffer.set(lenBuffer, 0); + buffer.set(output, lenBuffer.length); return buffer.buffer; } export function parse(input: ArrayBuffer): Uint8Array[] { let result: Uint8Array[] = []; let uint8Array = new Uint8Array(input); - // 8 - the length prefix size + const maxLengthPrefixSize = 5; + const numBitsToShift = [0, 7, 14, 21, 28 ]; + for (let offset = 0; offset < input.byteLength;) { - - if (input.byteLength < offset + 8) { - throw new Error("Cannot read message size") - } - - // Note javascript bitwise operators only support 32-bit integers - for now cutting bigger messages. - // Tracking bug https://github.com/aspnet/SignalR/issues/613 - if (!(uint8Array[offset] == 0 && uint8Array[offset + 1] == 0 && uint8Array[offset + 2] == 0 - && uint8Array[offset + 3] == 0 && (uint8Array[offset + 4] & 0x80) == 0)) { - throw new Error("Messages bigger than 2147483647 bytes are not supported"); - } - + let numBytes = 0; let size = 0; - for (let i = 4; i < 8; i++) { - size = (size << 8) | uint8Array[offset + i]; + let byteRead; + do + { + byteRead = uint8Array[offset + numBytes]; + size = size | ((byteRead & 0x7f) << (numBitsToShift[numBytes])); + numBytes++; + } + while (numBytes < Math.min(maxLengthPrefixSize, input.byteLength - offset) && (byteRead & 0x80) != 0); + + if ((byteRead & 0x80) !== 0 && numBytes < maxLengthPrefixSize) { + throw new Error("Cannot read message size."); } - if (uint8Array.byteLength >= (offset + 8 + size)) { + if (numBytes === maxLengthPrefixSize && byteRead > 7) { + throw new Error("Messages bigger than 2GB are not supported."); + } + + if (uint8Array.byteLength >= (offset + numBytes + size)) { // IE does not support .slice() so use subarray result.push(uint8Array.slice - ? uint8Array.slice(offset + 8, offset + 8 + size) - : uint8Array.subarray(offset + 8, offset + 8 + size)); + ? uint8Array.slice(offset + numBytes, offset + numBytes + size) + : uint8Array.subarray(offset + numBytes, offset + numBytes + size)); } else { - throw new Error("Incomplete message"); + throw new Error("Incomplete message."); } - offset = offset + 8 + size; + offset = offset + numBytes + size; } return result; diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageFormatter.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageFormatter.cs index 6d23544a9e..62d25c05b0 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageFormatter.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageFormatter.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.Binary; using System.Buffers; using System.IO; @@ -10,18 +9,35 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Formatters { public static class BinaryMessageFormatter { - public static void WriteMessage(ReadOnlySpan payload, Stream output) + public unsafe static void WriteMessage(ReadOnlySpan payload, Stream output) { - // TODO: Optimize for size - (e.g. use Varints) - var length = sizeof(long); - var buffer = ArrayPool.Shared.Rent(length); - BufferWriter.WriteBigEndian(buffer, payload.Length); - output.Write(buffer, 0, length); - ArrayPool.Shared.Return(buffer); + // This code writes length prefix of the message as a VarInt. Read the comment in + // the BinaryMessageParser.TryParseMessage for details. + + var lenBuffer = stackalloc byte[5]; + var lenNumBytes = 0; + var length = payload.Length; + do + { + ref var current = ref lenBuffer[lenNumBytes]; + current = (byte)(length & 0x7f); + length >>= 7; + if (length > 0) + { + current |= 0x80; + } + lenNumBytes++; + } + while (length > 0); + + var buffer = ArrayPool.Shared.Rent(lenNumBytes + payload.Length); + var bufferSpan = buffer.AsSpan(); + + new Span(lenBuffer, lenNumBytes).CopyTo(bufferSpan); + bufferSpan = bufferSpan.Slice(lenNumBytes); + payload.CopyTo(bufferSpan); + output.Write(buffer, 0, lenNumBytes + payload.Length); - buffer = ArrayPool.Shared.Rent(payload.Length); - payload.CopyTo(buffer); - output.Write(buffer, 0, payload.Length); ArrayPool.Shared.Return(buffer); } } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageParser.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageParser.cs index e730528a9f..a5c6d07e5c 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageParser.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Formatters/BinaryMessageParser.cs @@ -2,44 +2,70 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Binary; namespace Microsoft.AspNetCore.SignalR.Internal.Formatters { public static class BinaryMessageParser { + private static int[] _numBitsToShift = new[] { 0, 7, 14, 21, 28 }; + private const int MaxLengthPrefixSize = 5; + public static bool TryParseMessage(ref ReadOnlyBuffer buffer, out ReadOnlyBuffer payload) { - long length = 0; - payload = default(ReadOnlyBuffer); + payload = default; - if (buffer.Length < sizeof(long)) + if (buffer.IsEmpty) { return false; } - // Read the length - length = buffer.Span.Slice(0, sizeof(long)).ReadBigEndian(); + // The payload starts with a length prefix encoded as a VarInt. VarInts use the most significant bit + // as a marker whether the byte is the last byte of the VarInt or if it spans to the next byte. Bytes + // appear in the reverse order - i.e. the first byte contains the least significant bits of the value + // Examples: + // VarInt: 0x35 - %00110101 - the most significant bit is 0 so the value is %x0110101 i.e. 0x35 (53) + // VarInt: 0x80 0x25 - %10000000 %00101001 - the most significant bit of the first byte is 1 so the + // remaining bits (%x0000000) are the lowest bits of the value. The most significant bit of the second + // byte is 0 meaning this is last byte of the VarInt. The actual value bits (%x0101001) need to be + // prepended to the bits we already read so the values is %01010010000000 i.e. 0x1480 (5248) + // We support paylads up to 2GB so the biggest number we support is 7fffffff which when encoded as + // VarInt is 0xFF 0xFF 0xFF 0xFF 0x7F - hence the maximum length prefix is 5 bytes. - if (length > Int32.MaxValue) + var length = 0U; + var numBytes = 0; + + var lengthPrefixBuffer = buffer.Span.Slice(0, Math.Min(MaxLengthPrefixSize, buffer.Length)); + byte byteRead; + do { - throw new FormatException("Messages over 2GB in size are not supported"); + byteRead = lengthPrefixBuffer[numBytes]; + length = length | (((uint)(byteRead & 0x7f)) << _numBitsToShift[numBytes]); + numBytes++; + } + while (numBytes < lengthPrefixBuffer.Length && ((byteRead & 0x80) != 0)); + + // size bytes are missing + if ((byteRead & 0x80) != 0 && (numBytes < MaxLengthPrefixSize)) + { + return false; } - // Skip over the length - var remaining = buffer.Slice(sizeof(long)); + if ((byteRead & 0x80) != 0 || (numBytes == MaxLengthPrefixSize && byteRead > 7)) + { + throw new FormatException("Messages over 2GB in size are not supported."); + } // We don't have enough data - while (remaining.Length < (int)length) + if (buffer.Length < length + numBytes) { return false; } // Get the payload - payload = remaining.Slice(0, (int)length); + payload = buffer.Slice(numBytes, (int)length); // Skip the payload - buffer = remaining.Slice((int)length); + buffer = buffer.Slice(numBytes + (int)length); return true; } } 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 5e5bf31fd3..3f79d18104 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Microsoft.AspNetCore.SignalR.Common.csproj +++ b/src/Microsoft.AspNetCore.SignalR.Common/Microsoft.AspNetCore.SignalR.Common.csproj @@ -4,6 +4,7 @@ Common serialiation primitives for SignalR Clients Servers netstandard2.0 Microsoft.AspNetCore.SignalR + true diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/BinaryMessageFormatterTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/BinaryMessageFormatterTests.cs index 983d0c6114..7a35a6e16e 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/BinaryMessageFormatterTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/BinaryMessageFormatterTests.cs @@ -1,6 +1,8 @@ // 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.IO; using System.Linq; using System.Text; @@ -16,9 +18,9 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters { var expectedEncoding = new byte[] { - /* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* length: */ 0x00, /* body: */ - /* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, + /* length: */ 0x0E, /* body: */ 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x0D, 0x0A, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, }; @@ -38,10 +40,34 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters } [Theory] - [InlineData(0, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new byte[0])] - [InlineData(0, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })] - [InlineData(4, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new byte[0])] - [InlineData(4, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })] + [InlineData(0, new byte[] { 0x00 }, new byte[0])] + [InlineData(0, new byte[] { 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })] + [InlineData(0, new byte[] + { + 0x80, 0x01, // Size - 128 + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, + 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f + }, + new byte[] + { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, + 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f + })] + + [InlineData(4, new byte[] { 0x00 }, new byte[0])] + [InlineData(4, new byte[] { 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })] public void WriteBinaryMessage(int offset, byte[] encoded, byte[] payload) { var output = new MemoryStream(); @@ -57,10 +83,10 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters } [Theory] - [InlineData(0, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, "")] - [InlineData(0, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x42, 0x43 }, "ABC")] - [InlineData(0, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x41, 0x0A, 0x52, 0x0D, 0x43, 0x0D, 0x0A, 0x3B, 0x44, 0x45, 0x46 }, "A\nR\rC\r\n;DEF")] - [InlineData(4, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, "")] + [InlineData(0, new byte[] { 0x00 }, "")] + [InlineData(0, new byte[] { 0x03, 0x41, 0x42, 0x43 }, "ABC")] + [InlineData(0, new byte[] { 0x0B, 0x41, 0x0A, 0x52, 0x0D, 0x43, 0x0D, 0x0A, 0x3B, 0x44, 0x45, 0x46 }, "A\nR\rC\r\n;DEF")] + [InlineData(4, new byte[] { 0x00 }, "")] public void WriteTextMessage(int offset, byte[] encoded, string payload) { var message = Encoding.UTF8.GetBytes(payload); @@ -75,5 +101,36 @@ namespace Microsoft.AspNetCore.Sockets.Tests.Internal.Formatters Assert.Equal(encoded, output.ToArray().Skip(offset)); } + + [Theory] + [MemberData(nameof(RandomPayloads))] + public void RoundTrippingTest(byte[] payload) + { + using (var ms = new MemoryStream()) + { + BinaryMessageFormatter.WriteMessage(payload, ms); + var buffer = new ReadOnlyBuffer(ms.ToArray()); + Assert.True(BinaryMessageParser.TryParseMessage(ref buffer, out var roundtripped)); + Assert.Equal(payload, roundtripped.ToArray()); + } + } + + public static IEnumerable RandomPayloads() + { + // boundaries + yield return new[] { CreatePayload(0) }; + yield return new[] { CreatePayload(1) }; + yield return new[] { CreatePayload(0x7f) }; + yield return new[] { CreatePayload(0x80) }; + yield return new[] { CreatePayload(0x3fff) }; + yield return new[] { CreatePayload(0x4000) }; + + // random + yield return new[] { CreatePayload(0xc0de) }; + } + + + private static byte[] CreatePayload(int size) => + Enumerable.Range(0, size).Select(n => (byte)(n & 0xff)).ToArray(); } } diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/BinaryMessageParserTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/BinaryMessageParserTests.cs index 7c3fe7750f..58dfae9c99 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/BinaryMessageParserTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Formatters/BinaryMessageParserTests.cs @@ -12,9 +12,9 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters public class BinaryMessageParserTests { [Theory] - [InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, "")] - [InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x42, 0x43 }, "ABC")] - [InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x41, 0x0A, 0x52, 0x0D, 0x43, 0x0D, 0x0A, 0x3B, 0x44, 0x45, 0x46 }, "A\nR\rC\r\n;DEF")] + [InlineData(new byte[] { 0x00 }, "")] + [InlineData(new byte[] { 0x03, 0x41, 0x42, 0x43 }, "ABC")] + [InlineData(new byte[] { 0x0B, 0x41, 0x0A, 0x52, 0x0D, 0x43, 0x0D, 0x0A, 0x3B, 0x44, 0x45, 0x46 }, "A\nR\rC\r\n;DEF")] public void ReadMessage(byte[] encoded, string payload) { ReadOnlyBuffer span = encoded; @@ -25,8 +25,31 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters } [Theory] - [InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new byte[0])] - [InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })] + [InlineData(new byte[] { 0x00 }, new byte[0])] + [InlineData(new byte[] { 0x04, 0xAB, 0xCD, 0xEF, 0x12 }, new byte[] { 0xAB, 0xCD, 0xEF, 0x12 })] + [InlineData(new byte[] + { + 0x80, 0x01, // Size - 128 + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, + 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f + }, + new byte[] + { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, + 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f + })] public void ReadBinaryMessage(byte[] encoded, byte[] payload) { ReadOnlyBuffer span = encoded; @@ -35,14 +58,36 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters Assert.Equal(payload, message.ToArray()); } + [Theory] + [InlineData(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })] + [InlineData(new byte[] { 0x80, 0x80, 0x80, 0x80, 0x08 })] // 2GB + 1 + [InlineData(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })] + public void BinaryMessageParserThrowsForMessagesOver2GB(byte[] payload) + { + var buffer = new ReadOnlyBuffer(payload); + var ex = Assert.Throws(() => BinaryMessageParser.TryParseMessage(ref buffer, out var message)); + Assert.Equal("Messages over 2GB in size are not supported.", ex.Message); + } + + [Theory] + [InlineData(new byte[] { })] + [InlineData(new byte[] { 0x04, 0xAB, 0xCD, 0xEF })] + [InlineData(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0x07 })] // 2GB + [InlineData(new byte[] { 0x80 })] // size is cut + public void BinaryMessageParserReturnsFalseForPartialPayloads(byte[] payload) + { + var buffer = new ReadOnlyBuffer(payload); + Assert.False(BinaryMessageParser.TryParseMessage(ref buffer, out var message)); + } + [Fact] public void ReadMultipleMessages() { var encoded = new byte[] { - /* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* length: */ 0x00, /* body: */ - /* length: */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, + /* length: */ 0x0E, /* body: */ 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x0D, 0x0A, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, }; ReadOnlyBuffer buffer = encoded; @@ -62,7 +107,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Formatters [Theory] [InlineData(new byte[0])] // Empty - [InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00 })] // Not enough data for payload + [InlineData(new byte[] { 0x09, 0x00, 0x00 })] // Not enough data for payload public void ReadIncompleteMessages(byte[] encoded) { ReadOnlyBuffer buffer = encoded; diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs index 18c2464bd3..aade8f8cc7 100644 --- a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs @@ -117,12 +117,12 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol public void ParserThrowsForInvalidMessages(byte[] payload, string expectedExceptionMessage) { var payloadSize = payload.Length; - Debug.Assert(payloadSize <= 0xff, "This test does not support payloads larger than 255"); + Debug.Assert(payloadSize <= 0x7f, "This test does not support payloads larger than 127 bytes"); // prefix payload with the size - var buffer = new byte[8 + payloadSize]; - buffer[7] = (byte)(payloadSize & 0xff); - Array.Copy(payload, 0, buffer, 8, payloadSize); + var buffer = new byte[1 + payloadSize]; + buffer[0] = (byte)(payloadSize & 0x7f); + Array.Copy(payload, 0, buffer, 1, payloadSize); var binder = new TestBinder(new[] { typeof(string) }, typeof(string)); var exception = Assert.Throws(() => _hubProtocol.TryParseMessages(buffer, binder, out var messages)); @@ -131,13 +131,13 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol } [Theory] - [InlineData(new object[] { new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x01 }, 0 })] + [InlineData(new object[] { new byte[] { 0x05, 0x01 }, 0 })] [InlineData(new object[] { new byte[] { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x93, 0x03, 0xa1, 0x78, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x93, 0x03, 0xa1, 0x78, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x93, 0x03, 0xa1 + 0x05, 0x93, 0x03, 0xa1, 0x78, 0x02, + 0x05, 0x93, 0x03, 0xa1, 0x78, 0x02, + 0x05, 0x93, 0x03, 0xa1 }, 2 })] public void ParserDoesNotConsumePartialData(byte[] payload, int expectedMessagesCount) { @@ -154,7 +154,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol new InvocationMessage("0", false, "A", 1, new CustomObject()), new byte[] { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5b, 0x95, 0x01, 0xa1, 0x30, 0xc2, 0xa1, 0x41, + 0x5b, 0x95, 0x01, 0xa1, 0x30, 0xc2, 0xa1, 0x41, 0x92, // argument array 0x01, // 1 - first argument // 0x85 - a map of 5 items (properties) @@ -171,7 +171,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol CompletionMessage.WithResult("0", new CustomObject()), new byte[] { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x94, 0x03, 0xa1, 0x30, 0x03, + 0x57, 0x94, 0x03, 0xa1, 0x30, 0x03, // 0x85 - a map of 5 items (properties) 0x85, 0xac, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, 0xd3, 0x08, 0xd4, 0x80, 0x6d, 0xb2, 0x76, 0xc0, 0x00, 0xaa, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x50, 0x72, @@ -186,7 +186,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol new StreamItemMessage("0", new CustomObject()), new byte[] { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0x93, 0x02, 0xa1, 0x30, + 0x56, 0x93, 0x02, 0xa1, 0x30, // 0x85 - a map of 5 items (properties) 0x85, 0xac, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, 0xd3, 0x08, 0xd4, 0x80, 0x6d, 0xb2, 0x76, 0xc0, 0x00, 0xaa, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x50, 0x72, @@ -212,10 +212,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol [Fact] public void CanWriteObjectsWithoutDefaultCtors() { - var expectedPayload = new byte[] - { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x94, 0x03, 0xa1, 0x30, 0x03, 0x91, 0x2a - }; + var expectedPayload = new byte[] { 0x07, 0x94, 0x03, 0xa1, 0x30, 0x03, 0x91, 0x2a }; using (var memoryStream = new MemoryStream()) { From 602ca479b8372e7672b4fd2afefccd62488ef6a3 Mon Sep 17 00:00:00 2001 From: Pawel Kadluczka Date: Thu, 28 Sep 2017 14:28:57 -0700 Subject: [PATCH 2/2] Checking if window.document defined before trying to resolve url --- .vscode/launch.json | 22 +++++++++++++++++++ .../Connection.spec.ts | 7 ++++++ .../HttpConnection.ts | 2 +- 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..313c0a9d9e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceRoot}/client-ts/node_modules/jasmine/bin/jasmine.js", + "args": ["JASMINE_CONFIG_PATH=${workspaceRoot}/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/jasmine.json"], + "cwd": "${workspaceRoot}/client-ts", + "outFiles": [] + }, + { + "type": "node", + "request": "attach", + "name": "Attach to Port", + "address": "localhost", + "port": 5858, + "outFiles": [] + } + ] +} \ No newline at end of file diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Connection.spec.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Connection.spec.ts index bfa3c9f9f4..335dc9ffc1 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Connection.spec.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Connection.spec.ts @@ -14,6 +14,13 @@ describe("Connection", () => { .toThrow(new Error("Cannot resolve '/test'.")); }); + it("cannot be created with relative url if window object is not present", () => { + (global).window = {}; + expect(() => new HttpConnection("/test")) + .toThrow(new Error("Cannot resolve '/test'.")); + delete (global).window; + }); + it("starting connection fails if getting id fails", async (done) => { let options: IHttpConnectionOptions = { httpClient: { diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts index 733454c6e8..585815aba3 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts @@ -162,7 +162,7 @@ export class HttpConnection implements IConnection { return url; } - if (typeof window === 'undefined') { + if (typeof window === 'undefined' || !window || !window.document) { throw new Error(`Cannot resolve '${url}'.`); }