From 82f99a142499ac46642081ce68aaecd72f0b1fa7 Mon Sep 17 00:00:00 2001 From: moozzyk Date: Thu, 20 Apr 2017 09:50:53 -0700 Subject: [PATCH] Work around for a Firefox bug Firefox won't fire EventSource open event until it receives some data. The workaround is to send an empty comment when starting ServerSentEvent transport. Fixes: #352 --- samples/ChatSample/Views/Home/Index.cshtml | 47 ++++++++---- .../ServerSentEventsMessageParser.cs | 10 ++- .../Transports/ServerSentEventsTransport.cs | 3 + .../Formatters/ServerSentEventsParserTests.cs | 72 ++++++++++++------- .../ServerSentEventsTests.cs | 6 +- 5 files changed, 94 insertions(+), 44 deletions(-) diff --git a/samples/ChatSample/Views/Home/Index.cshtml b/samples/ChatSample/Views/Home/Index.cshtml index 9ad60aa18b..1c29e29eef 100644 --- a/samples/ChatSample/Views/Home/Index.cshtml +++ b/samples/ChatSample/Views/Home/Index.cshtml @@ -14,26 +14,45 @@ \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Sockets.Common/Internal/Formatters/ServerSentEventsMessageParser.cs b/src/Microsoft.AspNetCore.Sockets.Common/Internal/Formatters/ServerSentEventsMessageParser.cs index 3d22d6a5df..b786986dbc 100644 --- a/src/Microsoft.AspNetCore.Sockets.Common/Internal/Formatters/ServerSentEventsMessageParser.cs +++ b/src/Microsoft.AspNetCore.Sockets.Common/Internal/Formatters/ServerSentEventsMessageParser.cs @@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters { private const byte ByteCR = (byte)'\r'; private const byte ByteLF = (byte)'\n'; + private const byte ByteColon = (byte)':'; private const byte ByteT = (byte)'T'; private const byte ByteB = (byte)'B'; private const byte ByteC = (byte)'C'; @@ -38,7 +39,6 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters while (!reader.End) { - if (ReadCursorOperations.Seek(start, end, out var lineEnd, ByteLF) == -1) { // For the case of data: Foo\r\n\r\ @@ -63,6 +63,14 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Formatters throw new FormatException("There was an error in the frame format"); } + // Skip comments + if (line[0] == ByteColon) + { + start = lineEnd; + consumed = lineEnd; + continue; + } + if (IsMessageEnd(line)) { _internalParserState = InternalParseState.ReadEndOfMessage; diff --git a/src/Microsoft.AspNetCore.Sockets/Transports/ServerSentEventsTransport.cs b/src/Microsoft.AspNetCore.Sockets/Transports/ServerSentEventsTransport.cs index eaa5b98186..cdd4b020f7 100644 --- a/src/Microsoft.AspNetCore.Sockets/Transports/ServerSentEventsTransport.cs +++ b/src/Microsoft.AspNetCore.Sockets/Transports/ServerSentEventsTransport.cs @@ -37,6 +37,9 @@ namespace Microsoft.AspNetCore.Sockets.Transports context.Response.Headers["Content-Encoding"] = "identity"; + // Workaround for a Firefox bug where EventSource won't fire the open event + // until it receives some data + await context.Response.WriteAsync(":\r\n"); await context.Response.Body.FlushAsync(); var pipe = context.Response.Body.AsPipelineWriter(); diff --git a/test/Microsoft.AspNetCore.Sockets.Common.Tests/Internal/Formatters/ServerSentEventsParserTests.cs b/test/Microsoft.AspNetCore.Sockets.Common.Tests/Internal/Formatters/ServerSentEventsParserTests.cs index 2a6f3e1788..302608b9a2 100644 --- a/test/Microsoft.AspNetCore.Sockets.Common.Tests/Internal/Formatters/ServerSentEventsParserTests.cs +++ b/test/Microsoft.AspNetCore.Sockets.Common.Tests/Internal/Formatters/ServerSentEventsParserTests.cs @@ -16,14 +16,20 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters [Theory] [InlineData("data: T\r\n\r\n", "", MessageType.Text)] [InlineData("data: B\r\n\r\n", "", MessageType.Binary)] + [InlineData("data: T\r\n\r\n:\r\n", "", MessageType.Text)] + [InlineData("data: T\r\n\r\n:comment\r\n", "", MessageType.Text)] [InlineData("data: T\r\ndata: \r\r\n\r\n", "\r", MessageType.Text)] + [InlineData("data: T\r\n:comment\r\ndata: \r\r\n\r\n", "\r", MessageType.Text)] [InlineData("data: T\r\ndata: A\rB\r\n\r\n", "A\rB", MessageType.Text)] [InlineData("data: T\r\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)] - [InlineData("data: T\r\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)] [InlineData("data: T\r\ndata: Hello, World\r\n\r\ndata: ", "Hello, World", MessageType.Text)] + [InlineData("data: T\r\ndata: Hello, World\r\n\r\n:comment\r\ndata: ", "Hello, World", MessageType.Text)] + [InlineData("data: T\r\ndata: Hello, World\r\n\r\n:comment", "Hello, World", MessageType.Text)] + [InlineData("data: T\r\ndata: Hello, World\r\n\r\n:comment\r\n", "Hello, World", MessageType.Text)] + [InlineData("data: T\r\ndata: Hello, World\r\n:comment\r\n\r\n", "Hello, World", MessageType.Text)] [InlineData("data: B\r\ndata: SGVsbG8sIFdvcmxk\r\n\r\n", "Hello, World", MessageType.Binary)] [InlineData("data: B\r\ndata: SGVsbG8g\r\ndata: V29ybGQ=\r\n\r\n", "Hello World", MessageType.Binary)] - public void ParseSSEMessageSuccessCases(string encodedMessage, string expectedMessage, MessageType messageType) + public void ParseSSEMessageSuccessCases(string encodedMessage, string expectedMessage, MessageType messageType) { var buffer = Encoding.UTF8.GetBytes(encodedMessage); var readableBuffer = ReadableBuffer.Create(buffer); @@ -68,6 +74,9 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters [Theory] [InlineData("")] + [InlineData(":")] + [InlineData(":comment")] + [InlineData(":comment\r\n")] [InlineData("data:")] [InlineData("data: \r")] [InlineData("data: T\r\nda")] @@ -77,6 +86,10 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters [InlineData("data: T\r\ndata: Hello, World\r\n")] [InlineData("data: T\r\ndata: Hello, World\r\n\r")] [InlineData("data: B\r\ndata: SGVsbG8sIFd")] + [InlineData(":\r\ndata:")] + [InlineData("data: T\r\n:\r\n")] + [InlineData("data: T\r\n:\r\ndata:")] + [InlineData("data: T\r\ndata: Hello, World\r\n:comment")] public void ParseSSEMessageIncompleteParseResult(string encodedMessage) { var buffer = Encoding.UTF8.GetBytes(encodedMessage); @@ -89,40 +102,47 @@ namespace Microsoft.AspNetCore.Sockets.Common.Tests.Internal.Formatters } [Theory] - [InlineData("d", "ata: T\r\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)] - [InlineData("data: T", "\r\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)] - [InlineData("data: T\r", "\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)] - [InlineData("data: T\r\n", "data: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)] - [InlineData("data: T\r\nd", "ata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)] - [InlineData("data: T\r\ndata: ", "Hello, World\r\n\r\n", "Hello, World", MessageType.Text)] - [InlineData("data: T\r\ndata: Hello, World", "\r\n\r\n", "Hello, World", MessageType.Text)] - [InlineData("data: T\r\ndata: Hello, World\r\n", "\r\n", "Hello, World", MessageType.Text)] - [InlineData("data: T", "\r\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)] - [InlineData("data: ", "T\r\ndata: Hello, World\r\n\r\n", "Hello, World", MessageType.Text)] - [InlineData("data: B\r\ndata: SGVs", "bG8sIFdvcmxk\r\n\r\n", "Hello, World", MessageType.Binary)] - public async Task ParseMessageAcrossMultipleReadsSuccess(string encodedMessagePart1, string encodedMessagePart2, string expectedMessage, MessageType expectedMessageType) + [InlineData(new[] { "d", "ata: T\r\ndata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)] + [InlineData(new[] { "data: T", "\r\ndata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)] + [InlineData(new[] { "data: T\r", "\ndata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)] + [InlineData(new[] { "data: T\r\n", "data: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)] + [InlineData(new[] { "data: T\r\nd", "ata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)] + [InlineData(new[] { "data: T\r\ndata: ", "Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)] + [InlineData(new[] { "data: T\r\ndata: Hello, World", "\r\n\r\n" }, "Hello, World", MessageType.Text)] + [InlineData(new[] { "data: T\r\ndata: Hello, World\r\n", "\r\n" }, "Hello, World", MessageType.Text)] + [InlineData(new[] { "data: ", "T\r\ndata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)] + [InlineData(new[] { ":", "comment", "\r\n", "d", "ata: T\r\ndata: Hello, World\r\n\r\n" }, "Hello, World", MessageType.Text)] + [InlineData(new[] { "data: T\r\n", ":comment", "\r\n", "data: Hello, World", "\r\n\r\n" }, "Hello, World", MessageType.Text)] + [InlineData(new[] { "data: T\r\ndata: Hello, World\r\n", ":comment\r\n", "\r\n" }, "Hello, World", MessageType.Text)] + [InlineData(new[] { "data: B\r\ndata: SGVs", "bG8sIFdvcmxk\r\n\r\n" }, "Hello, World", MessageType.Binary)] + public async Task ParseMessageAcrossMultipleReadsSuccess(string[] messageParts, string expectedMessage, MessageType expectedMessageType) { using (var pipeFactory = new PipeFactory()) { + var parser = new ServerSentEventsMessageParser(); var pipe = pipeFactory.Create(); - // Read the first part of the message - await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(encodedMessagePart1)); + Message message = default(Message); + ReadCursor consumed = default(ReadCursor), examined = default(ReadCursor); - var result = await pipe.Reader.ReadAsync(); - var parser = new ServerSentEventsMessageParser(); + for (var i = 0; i < messageParts.Length; i++) + { + var messagePart = messageParts[i]; + await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(messagePart)); + var result = await pipe.Reader.ReadAsync(); - var parseResult = parser.ParseMessage(result.Buffer, out var consumed, out var examined, out Message message); - Assert.Equal(ServerSentEventsMessageParser.ParseResult.Incomplete, parseResult); + var parseResult = parser.ParseMessage(result.Buffer, out consumed, out examined, out message); + pipe.Reader.Advance(consumed, examined); - pipe.Reader.Advance(consumed, examined); + // parse result should be complete only after we parsed the last message part + var expectedResult = + i == messageParts.Length - 1 + ? ServerSentEventsMessageParser.ParseResult.Completed + : ServerSentEventsMessageParser.ParseResult.Incomplete; - // Send the rest of the data and parse the complete message - await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(encodedMessagePart2)); - result = await pipe.Reader.ReadAsync(); + Assert.Equal(expectedResult, parseResult); + } - parseResult = parser.ParseMessage(result.Buffer, out consumed, out examined, out message); - Assert.Equal(ServerSentEventsMessageParser.ParseResult.Completed, parseResult); Assert.Equal(expectedMessageType, message.Type); Assert.Equal(consumed, examined); diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/ServerSentEventsTests.cs b/test/Microsoft.AspNetCore.Sockets.Tests/ServerSentEventsTests.cs index 05dc925f5c..17883e6c3d 100644 --- a/test/Microsoft.AspNetCore.Sockets.Tests/ServerSentEventsTests.cs +++ b/test/Microsoft.AspNetCore.Sockets.Tests/ServerSentEventsTests.cs @@ -49,9 +49,9 @@ namespace Microsoft.AspNetCore.Sockets.Tests } [Theory] - [InlineData("Hello World", "data: T\r\ndata: Hello World\r\n\r\n")] - [InlineData("Hello\nWorld", "data: T\r\ndata: Hello\r\ndata: World\r\n\r\n")] - [InlineData("Hello\r\nWorld", "data: T\r\ndata: Hello\r\ndata: World\r\n\r\n")] + [InlineData("Hello World", ":\r\ndata: T\r\ndata: Hello World\r\n\r\n")] + [InlineData("Hello\nWorld", ":\r\ndata: T\r\ndata: Hello\r\ndata: World\r\n\r\n")] + [InlineData("Hello\r\nWorld", ":\r\ndata: T\r\ndata: Hello\r\ndata: World\r\n\r\n")] public async Task SSEAddsAppropriateFraming(string message, string expected) { var channel = Channel.CreateUnbounded();