diff --git a/src/Bedrock.Framework.Experimental/Infrastructure/BufferExtensions.cs b/src/Bedrock.Framework.Experimental/Infrastructure/BufferExtensions.cs index 9dc7e3b1..4ebcc08c 100644 --- a/src/Bedrock.Framework.Experimental/Infrastructure/BufferExtensions.cs +++ b/src/Bedrock.Framework.Experimental/Infrastructure/BufferExtensions.cs @@ -41,6 +41,35 @@ internal static ReadOnlyMemory ToMemory(in this ReadOnlySequence buf return buffer.ToArray(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool StartsWith(in this ReadOnlySequence sequence, ReadOnlySpan bytes) + { + if (sequence.Length < bytes.Length) + { + return false; + } + + foreach (var segment in sequence) + { + if (bytes.Length <= segment.Length) + { + return bytes.SequenceEqual(segment.Span[..bytes.Length]); + } + else if (!bytes[..segment.Length].SequenceEqual(segment.Span)) + { + return false; + } + bytes = bytes[segment.Length..]; + } + ThrowUnreachable(); + return false; + + void ThrowUnreachable() + { + throw new InvalidOperationException("This location is thought to be unreachable"); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static unsafe void WriteNumeric(ref this BufferWriter buffer, uint number) where T : struct, IBufferWriter diff --git a/src/Bedrock.Framework.Experimental/Protocols/Http1RequestMessageReader.cs b/src/Bedrock.Framework.Experimental/Protocols/Http1RequestMessageReader.cs index eb378027..13a4bacb 100644 --- a/src/Bedrock.Framework.Experimental/Protocols/Http1RequestMessageReader.cs +++ b/src/Bedrock.Framework.Experimental/Protocols/Http1RequestMessageReader.cs @@ -1,14 +1,18 @@ -using System; +using Bedrock.Framework.Infrastructure; +using Bedrock.Framework.Protocols.Http.Http1; +using System; using System.Buffers; +using System.Diagnostics; using System.Net.Http; using System.Text; namespace Bedrock.Framework.Protocols { - public class Http1RequestMessageReader : IMessageReader + public class Http1RequestMessageReader : IMessageReader> { + // Question: Do we want to inject this? Make it a singleton? What is the philosophy of this library WRT dependency injection? + private static Http1HeaderReader _headerReader = new Http1HeaderReader(); private ReadOnlySpan NewLine => new byte[] { (byte)'\r', (byte)'\n' }; - private ReadOnlySpan TrimChars => new byte[] { (byte)' ', (byte)'\t' }; private HttpRequestMessage _httpRequestMessage = new HttpRequestMessage(); @@ -19,13 +23,13 @@ public Http1RequestMessageReader(HttpContent content) _httpRequestMessage.Content = content; } - public bool TryParseMessage(in ReadOnlySequence input, ref SequencePosition consumed, ref SequencePosition examined, out HttpRequestMessage message) + public bool TryParseMessage(in ReadOnlySequence input, ref SequencePosition consumed, ref SequencePosition examined, out ParseResult message) { - var sequenceReader = new SequenceReader(input); - message = null; + message = default; if (_state == State.StartLine) { + var sequenceReader = new SequenceReader(input); if (!sequenceReader.TryReadTo(out ReadOnlySpan method, (byte)' ')) { return false; @@ -53,56 +57,45 @@ public bool TryParseMessage(in ReadOnlySequence input, ref SequencePositio } else if (_state == State.Headers) { - while (sequenceReader.TryReadTo(out var headerLine, NewLine)) + while (true) { - if (headerLine.Length == 0) + var remaining = input.Slice(consumed); + if (remaining.StartsWith(NewLine)) { - consumed = sequenceReader.Position; + _state = State.Body; + consumed = remaining.GetPosition(2); examined = consumed; + message = new ParseResult(_httpRequestMessage); + break; + } - message = _httpRequestMessage; + if (!_headerReader.TryParseMessage(remaining, ref consumed, ref examined, out var headerResult)) + { + return false; + } - // End of headers - _state = State.Body; - break; + if (headerResult.TryGetError(out var error)) + { + message = new ParseResult(error); + return true; } - // Parse the header - ParseHeader(headerLine, out var headerName, out var headerValue); + var success = headerResult.TryGetValue(out var header); + Debug.Assert(success == true); - var key = Encoding.ASCII.GetString(headerName.Trim(TrimChars)); - var value = Encoding.ASCII.GetString(headerValue.Trim(TrimChars)); + var key = Encoding.ASCII.GetString(header.Name); + var value = Encoding.ASCII.GetString(header.Value); if (!_httpRequestMessage.Headers.TryAddWithoutValidation(key, value)) { _httpRequestMessage.Content.Headers.TryAddWithoutValidation(key, value); } - - consumed = sequenceReader.Position; } } return _state == State.Body; } - internal static void ParseHeader(in ReadOnlySequence headerLine, out ReadOnlySpan headerName, out ReadOnlySpan headerValue) - { - if (headerLine.IsSingleSegment) - { - var span = headerLine.FirstSpan; - var colon = span.IndexOf((byte)':'); - headerName = span.Slice(0, colon); - headerValue = span.Slice(colon + 1); - } - else - { - var headerReader = new SequenceReader(headerLine); - headerReader.TryReadTo(out headerName, (byte)':'); - var remaining = headerReader.Sequence.Slice(headerReader.Position); - headerValue = remaining.IsSingleSegment ? remaining.FirstSpan : remaining.ToArray(); - } - } - private enum State { StartLine, diff --git a/src/Bedrock.Framework.Experimental/Protocols/Http1ResponseMessageReader.cs b/src/Bedrock.Framework.Experimental/Protocols/Http1ResponseMessageReader.cs index 69e2bcf0..36da82c5 100644 --- a/src/Bedrock.Framework.Experimental/Protocols/Http1ResponseMessageReader.cs +++ b/src/Bedrock.Framework.Experimental/Protocols/Http1ResponseMessageReader.cs @@ -1,17 +1,21 @@ -using System; +using Bedrock.Framework.Infrastructure; +using Bedrock.Framework.Protocols.Http.Http1; +using System; using System.Buffers; using System.Buffers.Text; using System.Collections.Generic; +using System.Diagnostics; using System.Net; using System.Net.Http; using System.Text; namespace Bedrock.Framework.Protocols { - public class Http1ResponseMessageReader : IMessageReader + public class Http1ResponseMessageReader : IMessageReader> { + // Question: Do we want to inject this? Make it a singleton? What is the philosophy of this library WRT dependency injection? + private static Http1HeaderReader _headerReader = new Http1HeaderReader(); private ReadOnlySpan NewLine => new byte[] { (byte)'\r', (byte)'\n' }; - private ReadOnlySpan TrimChars => new byte[] { (byte)' ', (byte)'\t' }; private HttpResponseMessage _httpResponseMessage = new HttpResponseMessage(); @@ -22,14 +26,15 @@ public Http1ResponseMessageReader(HttpContent content) _httpResponseMessage.Content = content; } - public bool TryParseMessage(in ReadOnlySequence input, ref SequencePosition consumed, ref SequencePosition examined, out HttpResponseMessage message) + public bool TryParseMessage(in ReadOnlySequence input, ref SequencePosition consumed, ref SequencePosition examined, out ParseResult message) { - var sequenceReader = new SequenceReader(input); - message = null; + message = default; switch (_state) { case State.StartLine: + var sequenceReader = new SequenceReader(input); + if (!sequenceReader.TryReadTo(out ReadOnlySpan version, (byte)' ')) { return false; @@ -60,35 +65,42 @@ public bool TryParseMessage(in ReadOnlySequence input, ref SequencePositio goto case State.Headers; case State.Headers: - while (sequenceReader.TryReadTo(out var headerLine, NewLine)) + while (true) { - if (headerLine.Length == 0) + var remaining = input.Slice(consumed); + + if (remaining.StartsWith(NewLine)) { - consumed = sequenceReader.Position; + consumed = remaining.GetPosition(2); examined = consumed; - - message = _httpResponseMessage; - - // End of headers + message = new ParseResult(_httpResponseMessage); _state = State.Body; break; } - // Parse the header - Http1RequestMessageReader.ParseHeader(headerLine, out var headerName, out var headerValue); + if (!_headerReader.TryParseMessage(remaining, ref consumed, ref examined, out var headerResult)) + { + return false; + } - var key = Encoding.ASCII.GetString(headerName.Trim(TrimChars)); - var value = Encoding.ASCII.GetString(headerValue.Trim(TrimChars)); + if (headerResult.TryGetError(out var error)) + { + message = new ParseResult(error); + return true; + } + + var success = headerResult.TryGetValue(out var header); + Debug.Assert(success == true); + + var key = Encoding.ASCII.GetString(header.Name); + var value = Encoding.ASCII.GetString(header.Value); if (!_httpResponseMessage.Headers.TryAddWithoutValidation(key, value)) { _httpResponseMessage.Content.Headers.TryAddWithoutValidation(key, value); } - - consumed = sequenceReader.Position; } - examined = sequenceReader.Position; break; default: break; diff --git a/src/Bedrock.Framework.Experimental/Protocols/HttpClientProtocol.cs b/src/Bedrock.Framework.Experimental/Protocols/HttpClientProtocol.cs index 4cd49be7..3a72c799 100644 --- a/src/Bedrock.Framework.Experimental/Protocols/HttpClientProtocol.cs +++ b/src/Bedrock.Framework.Experimental/Protocols/HttpClientProtocol.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -49,25 +50,33 @@ public async ValueTask SendAsync(HttpRequestMessage request throw new ConnectionAbortedException(); } - var response = result.Message; + var parseResult = result.Message; - // TODO: Handle upgrade - if (content.Headers.ContentLength != null) + if (parseResult.TryGetValue(out var response)) { - content.SetStream(new HttpBodyStream(_reader, new ContentLengthHttpBodyReader(response.Content.Headers.ContentLength.Value))); - } - else if (response.Headers.TransferEncodingChunked.HasValue) - { - content.SetStream(new HttpBodyStream(_reader, new ChunkedHttpBodyReader())); + // TODO: Handle upgrade + if (content.Headers.ContentLength != null) + { + content.SetStream(new HttpBodyStream(_reader, new ContentLengthHttpBodyReader(response.Content.Headers.ContentLength.Value))); + } + else if (response.Headers.TransferEncodingChunked.HasValue) + { + content.SetStream(new HttpBodyStream(_reader, new ChunkedHttpBodyReader())); + } + else + { + content.SetStream(new HttpBodyStream(_reader, new ContentLengthHttpBodyReader(0))); + } + + _reader.Advance(); + + return response; } else { - content.SetStream(new HttpBodyStream(_reader, new ContentLengthHttpBodyReader(0))); + parseResult.TryGetError(out var error); + throw new IOException($"Invalid Http Response. Reason: {error.Reason}, Line: {error.Line}"); } - - _reader.Advance(); - - return response; } } } diff --git a/src/Bedrock.Framework.Experimental/Protocols/HttpServerProtocol.cs b/src/Bedrock.Framework.Experimental/Protocols/HttpServerProtocol.cs index 0fbdbb27..deeb8c11 100644 --- a/src/Bedrock.Framework.Experimental/Protocols/HttpServerProtocol.cs +++ b/src/Bedrock.Framework.Experimental/Protocols/HttpServerProtocol.cs @@ -1,4 +1,5 @@ -using System.Net.Http; +using System.IO; +using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; @@ -29,25 +30,33 @@ public async ValueTask ReadRequestAsync() throw new ConnectionAbortedException(); } - var request = result.Message; + var parseResult = result.Message; - // TODO: Handle upgrade - if (content.Headers.ContentLength != null) + if (parseResult.TryGetValue(out var request)) { - content.SetStream(new HttpBodyStream(_reader, new ContentLengthHttpBodyReader(request.Content.Headers.ContentLength.Value))); - } - else if (request.Headers.TransferEncodingChunked.HasValue) - { - content.SetStream(new HttpBodyStream(_reader, new ChunkedHttpBodyReader())); + // TODO: Handle upgrade + if (content.Headers.ContentLength != null) + { + content.SetStream(new HttpBodyStream(_reader, new ContentLengthHttpBodyReader(request.Content.Headers.ContentLength.Value))); + } + else if (request.Headers.TransferEncodingChunked.HasValue) + { + content.SetStream(new HttpBodyStream(_reader, new ChunkedHttpBodyReader())); + } + else + { + content.SetStream(new HttpBodyStream(_reader, new ContentLengthHttpBodyReader(0))); + } + + _reader.Advance(); + + return request; } else { - content.SetStream(new HttpBodyStream(_reader, new ContentLengthHttpBodyReader(0))); + parseResult.TryGetError(out var error); + throw new IOException($"Invalid Http Request. Reason: {error.Reason}, Line: {error.Line}"); } - - _reader.Advance(); - - return request; } public async ValueTask WriteResponseAsync(HttpResponseMessage responseMessage)