// 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.Collections.Sequences; using System.IO.Pipelines; using System.Runtime.CompilerServices; using System.Text; namespace Microsoft.AspNetCore.Sockets.Internal.Formatters { public class ServerSentEventsMessageParser { private const byte ByteCR = (byte)'\r'; private const byte ByteLF = (byte)'\n'; private const byte ByteColon = (byte)':'; private static byte[] _dataPrefix = Encoding.UTF8.GetBytes("data: "); private static byte[] _sseLineEnding = Encoding.UTF8.GetBytes("\r\n"); private static byte[] _newLine = Encoding.UTF8.GetBytes(Environment.NewLine); private InternalParseState _internalParserState = InternalParseState.ReadMessagePayload; private List _data = new List(); public ParseResult ParseMessage(ReadOnlyBuffer buffer, out SequencePosition consumed, out SequencePosition examined, out byte[] message) { consumed = buffer.Start; examined = buffer.End; message = null; var start = consumed; var end = examined; while (buffer.Length > 0) { if (!(buffer.PositionOf(ByteLF) is SequencePosition lineEnd)) { // For the case of data: Foo\r\n\r\ if (_internalParserState == InternalParseState.ReadEndOfMessage) { if (ConvertBufferToSpan(buffer.Slice(start, buffer.End)).Length > 1) { throw new FormatException("Expected a \\r\\n frame ending"); } } // Partial message. We need to read more. return ParseResult.Incomplete; } lineEnd = buffer.GetPosition(lineEnd, 1); var line = ConvertBufferToSpan(buffer.Slice(start, lineEnd)); buffer = buffer.Slice(line.Length); if (line.Length <= 1) { 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; } // To ensure that the \n was preceded by a \r // since messages can't contain \n. // data: foo\n\bar should be encoded as // data: foo\r\n // data: bar\r\n else if (line[line.Length - _sseLineEnding.Length] != ByteCR) { throw new FormatException("Unexpected '\n' in message. A '\n' character can only be used as part of the newline sequence '\r\n'"); } else { EnsureStartsWithDataPrefix(line); } var payload = Array.Empty(); switch (_internalParserState) { case InternalParseState.ReadMessagePayload: EnsureStartsWithDataPrefix(line); // Slice away the 'data: ' var payloadLength = line.Length - (_dataPrefix.Length + _sseLineEnding.Length); var newData = line.Slice(_dataPrefix.Length, payloadLength).ToArray(); _data.Add(newData); start = lineEnd; consumed = lineEnd; break; case InternalParseState.ReadEndOfMessage: if (_data.Count == 1) { payload = _data[0]; } else if (_data.Count > 1) { // Find the final size of the payload var payloadSize = 0; foreach (var dataLine in _data) { payloadSize += dataLine.Length; } payloadSize += _newLine.Length * _data.Count; // Allocate space in the payload buffer for the data and the new lines. // Subtract newLine length because we don't want a trailing newline. payload = new byte[payloadSize - _newLine.Length]; var offset = 0; foreach (var dataLine in _data) { dataLine.CopyTo(payload, offset); offset += dataLine.Length; if (offset < payload.Length) { _newLine.CopyTo(payload, offset); offset += _newLine.Length; } } } message = payload; consumed = lineEnd; examined = consumed; return ParseResult.Completed; } if (buffer.Length > 0 && buffer.First.Span[0] == ByteCR) { _internalParserState = InternalParseState.ReadEndOfMessage; } } return ParseResult.Incomplete; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private ReadOnlySpan ConvertBufferToSpan(ReadOnlyBuffer buffer) { if (buffer.IsSingleSegment) { return buffer.First.Span; } return buffer.ToArray(); } public void Reset() { _internalParserState = InternalParseState.ReadMessagePayload; _data.Clear(); } private void EnsureStartsWithDataPrefix(ReadOnlySpan line) { if (!line.StartsWith(_dataPrefix)) { throw new FormatException("Expected the message prefix 'data: '"); } } private bool IsMessageEnd(ReadOnlySpan line) { return line.Length == _sseLineEnding.Length && line.SequenceEqual(_sseLineEnding); } public enum ParseResult { Completed, Incomplete, } private enum InternalParseState { ReadMessagePayload, ReadEndOfMessage, Error } } }