Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use HttpHeaderReader in RequestMessageReader and ResponseMessageReader #1

Open
wants to merge 2 commits into
base: feature/http-header-parser
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,35 @@ internal static ReadOnlyMemory<byte> ToMemory(in this ReadOnlySequence<byte> buf
return buffer.ToArray();
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool StartsWith(in this ReadOnlySequence<byte> sequence, ReadOnlySpan<byte> 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<T>(ref this BufferWriter<T> buffer, uint number)
where T : struct, IBufferWriter<byte>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<HttpRequestMessage>
public class Http1RequestMessageReader : IMessageReader<ParseResult<HttpRequestMessage>>
{
// 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<byte> NewLine => new byte[] { (byte)'\r', (byte)'\n' };
private ReadOnlySpan<byte> TrimChars => new byte[] { (byte)' ', (byte)'\t' };

private HttpRequestMessage _httpRequestMessage = new HttpRequestMessage();

Expand All @@ -19,13 +23,13 @@ public Http1RequestMessageReader(HttpContent content)
_httpRequestMessage.Content = content;
}

public bool TryParseMessage(in ReadOnlySequence<byte> input, ref SequencePosition consumed, ref SequencePosition examined, out HttpRequestMessage message)
public bool TryParseMessage(in ReadOnlySequence<byte> input, ref SequencePosition consumed, ref SequencePosition examined, out ParseResult<HttpRequestMessage> message)
{
var sequenceReader = new SequenceReader<byte>(input);
message = null;
message = default;

if (_state == State.StartLine)
{
var sequenceReader = new SequenceReader<byte>(input);
if (!sequenceReader.TryReadTo(out ReadOnlySpan<byte> method, (byte)' '))
{
return false;
Expand Down Expand Up @@ -53,56 +57,45 @@ public bool TryParseMessage(in ReadOnlySequence<byte> 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>(_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<HttpRequestMessage>(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<byte> headerLine, out ReadOnlySpan<byte> headerName, out ReadOnlySpan<byte> 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<byte>(headerLine);
headerReader.TryReadTo(out headerName, (byte)':');
var remaining = headerReader.Sequence.Slice(headerReader.Position);
headerValue = remaining.IsSingleSegment ? remaining.FirstSpan : remaining.ToArray();
}
}

private enum State
{
StartLine,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<HttpResponseMessage>
public class Http1ResponseMessageReader : IMessageReader<ParseResult<HttpResponseMessage>>
{
// 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<byte> NewLine => new byte[] { (byte)'\r', (byte)'\n' };
private ReadOnlySpan<byte> TrimChars => new byte[] { (byte)' ', (byte)'\t' };

private HttpResponseMessage _httpResponseMessage = new HttpResponseMessage();

Expand All @@ -22,14 +26,15 @@ public Http1ResponseMessageReader(HttpContent content)
_httpResponseMessage.Content = content;
}

public bool TryParseMessage(in ReadOnlySequence<byte> input, ref SequencePosition consumed, ref SequencePosition examined, out HttpResponseMessage message)
public bool TryParseMessage(in ReadOnlySequence<byte> input, ref SequencePosition consumed, ref SequencePosition examined, out ParseResult<HttpResponseMessage> message)
{
var sequenceReader = new SequenceReader<byte>(input);
message = null;
message = default;

switch (_state)
{
case State.StartLine:
var sequenceReader = new SequenceReader<byte>(input);

if (!sequenceReader.TryReadTo(out ReadOnlySpan<byte> version, (byte)' '))
{
return false;
Expand Down Expand Up @@ -60,35 +65,42 @@ public bool TryParseMessage(in ReadOnlySequence<byte> 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>(_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<HttpResponseMessage>(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;
Expand Down
35 changes: 22 additions & 13 deletions src/Bedrock.Framework.Experimental/Protocols/HttpClientProtocol.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
Expand Down Expand Up @@ -49,25 +50,33 @@ public async ValueTask<HttpResponseMessage> 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;
}
}
}
37 changes: 23 additions & 14 deletions src/Bedrock.Framework.Experimental/Protocols/HttpServerProtocol.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net.Http;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;

Expand Down Expand Up @@ -29,25 +30,33 @@ public async ValueTask<HttpRequestMessage> 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)
Expand Down