diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 111af2d01..56945bf48 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,10 +5,12 @@ on: branches: - main - 'release/*' + - 'feature/genhttp-next' push: branches: - main - - 'release/*' + - 'release/*' + - 'feature/genhttp-next' jobs: @@ -43,19 +45,19 @@ jobs: - name: Restore tools run: dotnet tool restore - - name: Begin scan - if: env.SONAR_TOKEN != null && env.SONAR_TOKEN != '' - run: dotnet sonarscanner begin /k:"GenHTTP" /d:sonar.token="$SONAR_TOKEN" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.exclusions="**/bin/**/*,**/obj/**/*,**/Playground/**/*,**/*.css,**/*.js,**/*.html" /o:"kaliumhexacyanoferrat" /k:"GenHTTP" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.branch.name="${GITHUB_REF##*/}" /d:sonar.dotnet.excludeTestProjects=true + #- name: Begin scan + # if: env.SONAR_TOKEN != null && env.SONAR_TOKEN != '' + # run: dotnet sonarscanner begin /k:"GenHTTP" /d:sonar.token="$SONAR_TOKEN" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.exclusions="**/bin/**/*,**/obj/**/*,**/Playground/**/*,**/*.css,**/*.js,**/*.html" /o:"kaliumhexacyanoferrat" /k:"GenHTTP" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.branch.name="${GITHUB_REF##*/}" /d:sonar.dotnet.excludeTestProjects=true - name: Build project run: dotnet build GenHTTP.slnx -c Release - - name: Test project - run: dotnet test GenHTTP.slnx --no-build --collect:"XPlat Code Coverage" -c Release -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover + #- name: Test project + # run: dotnet test GenHTTP.slnx --no-build --collect:"XPlat Code Coverage" -c Release -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover - - name: End scan - if: env.SONAR_TOKEN != null && env.SONAR_TOKEN != '' - run: dotnet sonarscanner end /d:sonar.token="$SONAR_TOKEN" + #- name: End scan + # if: env.SONAR_TOKEN != null && env.SONAR_TOKEN != '' + # run: dotnet sonarscanner end /d:sonar.token="$SONAR_TOKEN" verify: @@ -135,5 +137,8 @@ jobs: 9.0 10.0 - - name: Build & Test (${{ matrix.runtime }}) - run: dotnet test Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj -c Release --logger "console;verbosity=detailed" + - name: Build (${{ matrix.runtime }}) + run: dotnet build GenHTTP.slnx -c Release + + #- name: Build & Test (${{ matrix.runtime }}) + # run: dotnet test Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj -c Release --logger "console;verbosity=detailed" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c07984002..ff0cc314f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,7 +45,7 @@ jobs: working-directory: Packages run: | nuget pack GenHTTP.Core.nuspec -OutputDirectory ../artifacts - nuget pack GenHTTP.Core.Kestrel.nuspec -OutputDirectory ../artifacts + # nuget pack GenHTTP.Core.Kestrel.nuspec -OutputDirectory ../artifacts - name: List gathered packages run: dir artifacts diff --git a/API/Content/IO/IResource.cs b/API/Content/IO/IResource.cs index dccc819ea..51a68f424 100644 --- a/API/Content/IO/IResource.cs +++ b/API/Content/IO/IResource.cs @@ -1,4 +1,5 @@ -using GenHTTP.Api.Protocol; +using System.Buffers; +using GenHTTP.Api.Protocol; namespace GenHTTP.Api.Content.IO; @@ -66,4 +67,6 @@ async ValueTask WriteAsync(Stream target, uint bufferSize) await content.CopyToAsync(target, (int)bufferSize); } + void Write(IBufferWriter writer); + } diff --git a/API/Protocol/ClientProtocol.cs b/API/Protocol/ClientProtocol.cs index 481f7b2c8..771cbfb11 100644 --- a/API/Protocol/ClientProtocol.cs +++ b/API/Protocol/ClientProtocol.cs @@ -1,5 +1,7 @@ namespace GenHTTP.Api.Protocol; +// todo: re-visit + public enum ClientProtocol { Http, diff --git a/API/Protocol/Connection.cs b/API/Protocol/Connection.cs index 7720ae14b..1595b1c03 100644 --- a/API/Protocol/Connection.cs +++ b/API/Protocol/Connection.cs @@ -1,5 +1,7 @@ namespace GenHTTP.Api.Protocol; +// todo: re-visit + public enum Connection { diff --git a/API/Protocol/ContentType.cs b/API/Protocol/ContentType.cs index ff4aba613..4f7461709 100644 --- a/API/Protocol/ContentType.cs +++ b/API/Protocol/ContentType.cs @@ -2,6 +2,8 @@ namespace GenHTTP.Api.Protocol; +// todo: re-visit + #region Known Types /// diff --git a/API/Protocol/Cookie.cs b/API/Protocol/Cookie.cs index 48698733d..957acd9ef 100644 --- a/API/Protocol/Cookie.cs +++ b/API/Protocol/Cookie.cs @@ -1,5 +1,7 @@ namespace GenHTTP.Api.Protocol; +// todo: re-visit + /// /// Represents a cookie that can be send to or received from a client. /// diff --git a/API/Protocol/Forwarding.cs b/API/Protocol/Forwarding.cs index 469167cce..be6708c80 100644 --- a/API/Protocol/Forwarding.cs +++ b/API/Protocol/Forwarding.cs @@ -2,6 +2,8 @@ namespace GenHTTP.Api.Protocol; +// todo: re-visit + /// /// Stores information how a request has been proxied /// to the server. diff --git a/API/Protocol/HttpProtocol.cs b/API/Protocol/HttpProtocol.cs index 14a330b8b..d048df81d 100644 --- a/API/Protocol/HttpProtocol.cs +++ b/API/Protocol/HttpProtocol.cs @@ -1,5 +1,7 @@ namespace GenHTTP.Api.Protocol; +// todo: re-visit + /// /// The protocol version of a request. /// diff --git a/API/Protocol/ICookieCollection.cs b/API/Protocol/ICookieCollection.cs index 13675fce8..14e31e201 100644 --- a/API/Protocol/ICookieCollection.cs +++ b/API/Protocol/ICookieCollection.cs @@ -1,5 +1,7 @@ namespace GenHTTP.Api.Protocol; +// todo: re-visit + /// /// A collection representing the cookies of an /// or . diff --git a/API/Protocol/IEditableHeaderCollection.cs b/API/Protocol/IEditableHeaderCollection.cs index 4948fdc39..14f0b8aee 100644 --- a/API/Protocol/IEditableHeaderCollection.cs +++ b/API/Protocol/IEditableHeaderCollection.cs @@ -1,3 +1,5 @@ namespace GenHTTP.Api.Protocol; +// todo: re-visit + public interface IEditableHeaderCollection : IDictionary; diff --git a/API/Protocol/IForwardingCollection.cs b/API/Protocol/IForwardingCollection.cs index 2d11b031a..994fd9f87 100644 --- a/API/Protocol/IForwardingCollection.cs +++ b/API/Protocol/IForwardingCollection.cs @@ -1,3 +1,5 @@ namespace GenHTTP.Api.Protocol; +// todo: re-visit + public interface IForwardingCollection : IList; diff --git a/API/Protocol/IHeaderCollection.cs b/API/Protocol/IHeaderCollection.cs index cb8337873..7a99a4f14 100644 --- a/API/Protocol/IHeaderCollection.cs +++ b/API/Protocol/IHeaderCollection.cs @@ -1,6 +1,8 @@ namespace GenHTTP.Api.Protocol; +// todo: re-visit + /// /// The headers of an or . /// -public interface IHeaderCollection : IReadOnlyDictionary; \ No newline at end of file +public interface IHeaderCollection : IReadOnlyDictionary; diff --git a/API/Protocol/IKeyValueList.cs b/API/Protocol/IKeyValueList.cs new file mode 100644 index 000000000..79ae2f32f --- /dev/null +++ b/API/Protocol/IKeyValueList.cs @@ -0,0 +1,10 @@ +namespace GenHTTP.Api.Protocol; + +public interface IKeyValueList +{ + + string? GetValue(string key); + + string? GetValue(ReadOnlyMemory key); + +} diff --git a/API/Protocol/IRequest.cs b/API/Protocol/IRequest.cs index 3a4d05842..594b49129 100644 --- a/API/Protocol/IRequest.cs +++ b/API/Protocol/IRequest.cs @@ -1,145 +1,22 @@ -using GenHTTP.Api.Infrastructure; -using GenHTTP.Api.Routing; +using GenHTTP.Api.Protocol.Raw; namespace GenHTTP.Api.Protocol; -/// -/// A request send by the currently connected client. -/// -public interface IRequest : IDisposable +public interface IRequest { - #region Extensibility + IRawRequest Raw { get; } - /// - /// Additional properties that have been attached to the request. - /// - /// - /// Can be used to store additional state on request level if needed. - /// Should be avoided in favor of more strict coupling. - /// - IRequestProperties Properties { get; } + IKeyValueList Headers { get; } - #endregion + IKeyValueList Query { get; } - #region Functionality + IRequestBody? Body { get; } - /// - /// Generates a new response for this request to be send to the client. - /// - /// The newly created response - IResponseBuilder Respond(); - - #endregion - - #region General Infrastructure - - /// - /// The server handling the request. - /// - IServer Server { get; } - - /// - /// The endpoint the request originates from. - /// - IEndPoint EndPoint { get; } - - /// - /// The client which sent the request. - /// - IClientConnection Client { get; } - - /// - /// If the request has been forwarded by a proxy, the client property - /// will return the originating client while this property will return - /// the information of the proxy. - /// - IClientConnection LocalClient { get; } - - #endregion - - #region HTTP Protocol - - /// - /// The requested protocol type. - /// - HttpProtocol ProtocolType { get; } - - /// - /// The HTTP method used by the client to issue this request. - /// - FlexibleRequestMethod Method { get; } - - /// - /// The path requested by the client (with no query parameters attached). - /// - RoutingTarget Target { get; } - - #endregion + RequestMethod Method { get; } - #region Headers + string Host { get; } - /// - /// The user agent which issued this request, if any. - /// - string? UserAgent { get; } - - /// - /// The referrer which caused the invocation of this request, if any. - /// - string? Referer { get; } - - /// - /// The host requested by the client, if any. - /// - string? Host { get; } - - /// - /// Read an additional header value from the request. - /// - /// The name of the header field to be read - /// The value of the header field, if specified by the client - string? this[string additionalHeader] { get; } - - /// - /// The query parameters passed by the client. - /// - IRequestQuery Query { get; } - - /// - /// The cookies passed by the client. - /// - ICookieCollection Cookies { get; } - - /// - /// If the request has been forwarded by one or more proxies, this collection may contain - /// additional information about the initial request by the originating client. - /// - /// - /// Use to quickly access the requesting client without the need - /// of scrolling through the forwardings. - /// - IForwardingCollection Forwardings { get; } - - /// - /// The headers of this HTTP request. - /// - IHeaderCollection Headers { get; } - - #endregion - - #region Body - - /// - /// The content transmitted by the client, if any. - /// - Stream? Content { get; } - - /// - /// The type of content transmitted by the client, if any. - /// - FlexibleContentType? ContentType { get; } - - #endregion + IResponseBuilder Respond(); } diff --git a/API/Protocol/IRequestBody.cs b/API/Protocol/IRequestBody.cs new file mode 100644 index 000000000..a0dd95bd4 --- /dev/null +++ b/API/Protocol/IRequestBody.cs @@ -0,0 +1,18 @@ +namespace GenHTTP.Api.Protocol; + +public interface IRequestBody +{ + + /// + /// Provides the body of the request as a stream. + /// + /// The stream created to access the body + Stream AsStream(); + + /// + /// Reads the whole request body into a byte array. + /// + /// The byte array read from the request body + byte[] AsArray(); + +} diff --git a/API/Protocol/IRequestProperties.cs b/API/Protocol/IRequestProperties.cs index 51c0d161c..239ceb45b 100644 --- a/API/Protocol/IRequestProperties.cs +++ b/API/Protocol/IRequestProperties.cs @@ -2,6 +2,8 @@ namespace GenHTTP.Api.Protocol; +// todo: re-visit + /// /// Property bag to store additional data within the /// currently running request context. diff --git a/API/Protocol/IRequestQuery.cs b/API/Protocol/IRequestQuery.cs index 652ab0119..c3ce19270 100644 --- a/API/Protocol/IRequestQuery.cs +++ b/API/Protocol/IRequestQuery.cs @@ -1,5 +1,7 @@ namespace GenHTTP.Api.Protocol; +// todo: re-visit + /// /// Stores the query sent by the client. /// diff --git a/API/Protocol/IResponse.cs b/API/Protocol/IResponse.cs index 1e5f5fdca..af074efb3 100644 --- a/API/Protocol/IResponse.cs +++ b/API/Protocol/IResponse.cs @@ -1,89 +1,12 @@ -namespace GenHTTP.Api.Protocol; +using GenHTTP.Api.Protocol.Raw; -/// -/// The response to be send to the connected client for a given request. -/// -public interface IResponse : IDisposable -{ - - #region Protocol - - /// - /// The HTTP response code. - /// - FlexibleResponseStatus Status { get; set; } - - /// - /// Hints the server how the connection should be handled. - /// - Connection Connection { get; set; } - - #endregion - - #region Headers - - /// - /// Define, when this resource will expire. - /// - DateTime? Expires { get; set; } - - /// - /// Define, when this ressource has been changed the last time. - /// - DateTime? Modified { get; set; } - - /// - /// Retrieve or set the value of a header field. - /// - /// The name of the header field - /// The value of the header field - string? this[string field] { get; set; } +namespace GenHTTP.Api.Protocol; - /// - /// The headers of the HTTP response. - /// - IEditableHeaderCollection Headers { get; } - - /// - /// The cookies to be sent to the client. - /// - ICookieCollection Cookies { get; } - - /// - /// True, if there are cookies to be sent with this respone. - /// - bool HasCookies { get; } - - /// - /// Adds the given cookie to the cookie collection of this response. - /// - /// The cookie to be added - void SetCookie(Cookie cookie); - - #endregion - - #region Content - - /// - /// The type of the content. - /// - FlexibleContentType? ContentType { get; set; } - - /// - /// The encoding of the content (e.g. "br"). - /// - string? ContentEncoding { get; set; } - - /// - /// The number of bytes the content consists of. - /// - ulong? ContentLength { get; set; } +public interface IResponse +{ - /// - /// The response that will be sent to the requesting client. - /// - IResponseContent? Content { get; set; } + IRawResponse Raw { get; } - #endregion + IResponseBuilder Rebuild(); } diff --git a/API/Protocol/IResponseBuilder.cs b/API/Protocol/IResponseBuilder.cs index a24d2d528..b93983f6e 100644 --- a/API/Protocol/IResponseBuilder.cs +++ b/API/Protocol/IResponseBuilder.cs @@ -1,22 +1,15 @@ using GenHTTP.Api.Infrastructure; +using GenHTTP.Api.Protocol.Raw; namespace GenHTTP.Api.Protocol; -/// -/// Allows to configure a HTTP response to be send. -/// -public interface IResponseBuilder : IBuilder, IResponseModification +public interface IResponseBuilder : IBuilder { - /// - /// Specifies the content to be sent to the client. - /// - /// The content to be send to the client - IResponseBuilder Content(IResponseContent content); + IResponseBuilder Status(ResponseStatus status); + + IResponseBuilder Header(string name, string value); + + IRawResponseBuilder Raw(); - /// - /// Specifies the length of the content stream, if known. - /// - /// The length of the content stream - IResponseBuilder Length(ulong length); } diff --git a/API/Protocol/IResponseContent.cs b/API/Protocol/IResponseContent.cs index 16249d161..021d1fd43 100644 --- a/API/Protocol/IResponseContent.cs +++ b/API/Protocol/IResponseContent.cs @@ -1,43 +1,10 @@ -namespace GenHTTP.Api.Protocol; +namespace GenHTTP.Api.Protocol; -/// -/// Represents the content of a HTTP response to be sent to the client. -/// -/// -/// Allows to efficiently stream data into the network stream used by the server. -/// public interface IResponseContent { - /// - /// The number of bytes to be sent to the client (if known). - /// - /// - /// If null is returned by this method, the server needs - /// to use chunked encoding to send the data to the client. Therefore, - /// try to determine the correct length of the content to be sent - /// whenever possible. - /// Writing more or less bytes than indicated by this property to the - /// target stream will cause HTTP client errors or timeouts to occur. - /// ulong? Length { get; } - /// - /// A checksum of the content represented by this instance. - /// - /// - /// The checksum calculation should be as fast as possible but - /// still allow to reliably detect changes. For efficient processing, - /// this also means that the content should actually be expanded - /// when the Write call is invoked, not when the content instance - /// is constructed. - /// - ValueTask CalculateChecksumAsync(); + ValueTask WriteAsync(IResponseSink sink); - /// - /// Writes the content to the specified target stream. - /// - /// The stream to write the data to - /// The buffer size to be used to write the data - ValueTask WriteAsync(Stream target, uint bufferSize); } diff --git a/API/Protocol/IResponseModification.cs b/API/Protocol/IResponseModification.cs index e17d43a7a..2dfb9d3f2 100644 --- a/API/Protocol/IResponseModification.cs +++ b/API/Protocol/IResponseModification.cs @@ -1,5 +1,7 @@ namespace GenHTTP.Api.Protocol; +// todo: re-visit + /// /// Allows the response generated by a builder or handler to be /// adjusted. diff --git a/API/Protocol/IResponseSink.cs b/API/Protocol/IResponseSink.cs new file mode 100644 index 000000000..243489bbc --- /dev/null +++ b/API/Protocol/IResponseSink.cs @@ -0,0 +1,12 @@ +using System.Buffers; + +namespace GenHTTP.Api.Protocol; + +public interface IResponseSink +{ + + IBufferWriter Writer { get; } + + Stream Stream { get; } + +} diff --git a/API/Protocol/Raw/Extensions.cs b/API/Protocol/Raw/Extensions.cs new file mode 100644 index 000000000..63c445d86 --- /dev/null +++ b/API/Protocol/Raw/Extensions.cs @@ -0,0 +1,21 @@ +namespace GenHTTP.Api.Protocol.Raw; + +public static class Extensions +{ + + public static ReadOnlyMemory? GetEntry(this IRawKeyValueList list, ReadOnlyMemory key) + { + for (var i = 0; i < list.Count; i++) + { + var entry = list[i]; + + if (entry.Key.Span.SequenceEqual(key.Span)) + { + return entry.Value; + } + } + + return null; + } + +} diff --git a/API/Protocol/Raw/IRawKeyValueList.cs b/API/Protocol/Raw/IRawKeyValueList.cs new file mode 100644 index 000000000..1dde8ba31 --- /dev/null +++ b/API/Protocol/Raw/IRawKeyValueList.cs @@ -0,0 +1,23 @@ +namespace GenHTTP.Api.Protocol.Raw; + +public interface IRawKeyValueList +{ + + int Count { get; } + + KeyValuePair, ReadOnlyMemory> this[int index] { get; } + + bool ContainsKey(ReadOnlyMemory key) + { + var keySpan = key.Span; + + for (var i = 0; i < Count; i++) + { + if (this[i].Key.Span.SequenceEqual(keySpan)) + return true; + } + + return false; + } + +} diff --git a/API/Protocol/Raw/IRawRequest.cs b/API/Protocol/Raw/IRawRequest.cs new file mode 100644 index 000000000..1a1bf9b1e --- /dev/null +++ b/API/Protocol/Raw/IRawRequest.cs @@ -0,0 +1,19 @@ +namespace GenHTTP.Api.Protocol.Raw; + + +public interface IRawRequest +{ + + IRawRequestHeader Header { get;} + + IRawRequestBody? GetBody(HeaderAccess headerAccess); + + // todo: wrap body (e.g. content decoding) + +} + +public enum HeaderAccess +{ + Retain, + Release +} diff --git a/API/Protocol/Raw/IRawRequestBody.cs b/API/Protocol/Raw/IRawRequestBody.cs new file mode 100644 index 000000000..81406162c --- /dev/null +++ b/API/Protocol/Raw/IRawRequestBody.cs @@ -0,0 +1,15 @@ +namespace GenHTTP.Api.Protocol.Raw; + +public interface IRawRequestBody +{ + + /// + /// Fetches the next chunk of data from the underlying connection. + /// + /// The next chunk of data read from the underlying connection or null, if no more data is left to be read + /// + /// Transparently handles content length boundaries and chunked encoding. + /// + ValueTask?> TryReadAsync(); + +} diff --git a/API/Protocol/Raw/IRawRequestHeader.cs b/API/Protocol/Raw/IRawRequestHeader.cs new file mode 100644 index 000000000..3bc5948a2 --- /dev/null +++ b/API/Protocol/Raw/IRawRequestHeader.cs @@ -0,0 +1,18 @@ +namespace GenHTTP.Api.Protocol.Raw; + +public interface IRawRequestHeader +{ + + ReadOnlyMemory Method { get; } + + ReadOnlyMemory Path { get; } + + ReadOnlyMemory Version { get; } + + IRawRequestTarget Target { get; } + + IRawKeyValueList Query { get; } + + IRawKeyValueList Headers { get; } + +} diff --git a/API/Protocol/Raw/IRawRequestTarget.cs b/API/Protocol/Raw/IRawRequestTarget.cs new file mode 100644 index 000000000..0a5535352 --- /dev/null +++ b/API/Protocol/Raw/IRawRequestTarget.cs @@ -0,0 +1,10 @@ +namespace GenHTTP.Api.Protocol.Raw; + +public interface IRawRequestTarget +{ + + ReadOnlyMemory? Current { get; } + + void Advance(int segments = 1); + +} diff --git a/API/Protocol/Raw/IRawResponse.cs b/API/Protocol/Raw/IRawResponse.cs new file mode 100644 index 000000000..852d0e912 --- /dev/null +++ b/API/Protocol/Raw/IRawResponse.cs @@ -0,0 +1,12 @@ +namespace GenHTTP.Api.Protocol.Raw; + +public interface IRawResponse +{ + + ResponseStatus Status { get; } + + IRawKeyValueList Headers { get; } + + IResponseContent? Content { get; } + +} diff --git a/API/Protocol/Raw/IRawResponseBuilder.cs b/API/Protocol/Raw/IRawResponseBuilder.cs new file mode 100644 index 000000000..5a0f03c4e --- /dev/null +++ b/API/Protocol/Raw/IRawResponseBuilder.cs @@ -0,0 +1,16 @@ +using GenHTTP.Api.Infrastructure; + +namespace GenHTTP.Api.Protocol.Raw; + +public interface IRawResponseBuilder : IBuilder +{ + + IRawResponseBuilder Status(ResponseStatus code); + + IRawResponseBuilder Header(ReadOnlyMemory name, ReadOnlyMemory value); + + IRawResponseBuilder Content(IResponseContent? content); + + IResponseBuilder Unraw(); + +} diff --git a/API/Protocol/RequestMethod.cs b/API/Protocol/RequestMethod.cs index 9d3876631..7e725f8ff 100644 --- a/API/Protocol/RequestMethod.cs +++ b/API/Protocol/RequestMethod.cs @@ -6,219 +6,13 @@ public enum RequestMethod Head, Post, Put, - Patch, Delete, + Connect, Options, - PropFind, - PropPatch, - MkCol, - Copy, - Move, - Lock, - Unlock -} - -/// -/// The kind of request sent by the client. -/// -public class FlexibleRequestMethod -{ - private static readonly Dictionary RawCache = new(StringComparer.InvariantCultureIgnoreCase) - { - { - "HEAD", new FlexibleRequestMethod(RequestMethod.Head) - }, - { - "GET", new FlexibleRequestMethod(RequestMethod.Get) - }, - { - "POST", new FlexibleRequestMethod(RequestMethod.Post) - }, - { - "PUT", new FlexibleRequestMethod(RequestMethod.Put) - }, - { - "DELETE", new FlexibleRequestMethod(RequestMethod.Delete) - }, - { - "OPTIONS", new FlexibleRequestMethod(RequestMethod.Options) - } - }; - - private static readonly Dictionary KnownCache = new() - { - { - RequestMethod.Head, new FlexibleRequestMethod(RequestMethod.Head) - }, - { - RequestMethod.Get, new FlexibleRequestMethod(RequestMethod.Get) - }, - { - RequestMethod.Post, new FlexibleRequestMethod(RequestMethod.Post) - }, - { - RequestMethod.Put, new FlexibleRequestMethod(RequestMethod.Put) - }, - { - RequestMethod.Delete, new FlexibleRequestMethod(RequestMethod.Delete) - }, - { - RequestMethod.Options, new FlexibleRequestMethod(RequestMethod.Options) - } - }; - - #region Mapping - - private static readonly Dictionary Mapping = new(StringComparer.OrdinalIgnoreCase) - { - { - "GET", RequestMethod.Get - }, - { - "HEAD", RequestMethod.Head - }, - { - "POST", RequestMethod.Post - }, - { - "PUT", RequestMethod.Put - }, - { - "PATCH", RequestMethod.Patch - }, - { - "DELETE", RequestMethod.Delete - }, - { - "OPTIONS", RequestMethod.Options - }, - { - "PROPFIND", RequestMethod.PropFind - }, - { - "PROPPATCH", RequestMethod.PropPatch - }, - { - "MKCOL", RequestMethod.MkCol - }, - { - "COPY", RequestMethod.Copy - }, - { - "MOVE", RequestMethod.Move - }, - { - "LOCK", RequestMethod.Lock - }, - { - "UNLOCK", RequestMethod.Unlock - } - }; - - #endregion - - #region Get-/Setters - - /// - /// The known method of the request, if any. - /// - public RequestMethod? KnownMethod { get; } - - /// - /// The raw method of the request. - /// - public string RawMethod { get; } - - #endregion - - #region Initialization - - /// - /// Creates a new request method instance from a known type. - /// - /// The known type to be used - public FlexibleRequestMethod(RequestMethod method) - { - KnownMethod = method; - RawMethod = Enum.GetName(method) ?? throw new ArgumentException("The given method cannot be mapped", nameof(method)); - } - - /// - /// Create a new request method instance. - /// - /// The raw type transmitted by the client - public FlexibleRequestMethod(string rawType) - { - RawMethod = rawType; - - if (Mapping.TryGetValue(rawType, out var type)) - { - KnownMethod = type; - } - else - { - KnownMethod = null; - } - } - - #endregion - - #region Functionality - - /// - /// Fetches a cached instance for the given content type. - /// - /// The raw string to be resolved - /// The content type instance to be used - public static FlexibleRequestMethod Get(string rawMethod) - { - if (RawCache.TryGetValue(rawMethod, out var found)) - { - return found; - } - - var method = new FlexibleRequestMethod(rawMethod); - - RawCache[rawMethod] = method; - - return method; - } - - /// - /// Fetches a cached instance for the given content type. - /// - /// The known value to be resolved - /// The content type instance to be used - public static FlexibleRequestMethod Get(RequestMethod knownMethod) - { - if (KnownCache.TryGetValue(knownMethod, out var found)) - { - return found; - } - - var method = new FlexibleRequestMethod(knownMethod); - - KnownCache[knownMethod] = method; - - return method; - } - - #endregion - - #region Convenience - - public static bool operator ==(FlexibleRequestMethod method, RequestMethod knownMethod) => method.KnownMethod == knownMethod; - - public static bool operator !=(FlexibleRequestMethod method, RequestMethod knownMethod) => method.KnownMethod != knownMethod; - - public static bool operator ==(FlexibleRequestMethod method, string rawMethod) => method.RawMethod == rawMethod; - - public static bool operator !=(FlexibleRequestMethod method, string rawMethod) => method.RawMethod != rawMethod; - - public override bool Equals(object? obj) => obj is FlexibleRequestMethod method && RawMethod == method.RawMethod; - - public override int GetHashCode() => RawMethod.GetHashCode(); + Trace, + Patch, - #endregion + // if it cannot be parsed into one of the ones above + Other } diff --git a/API/Protocol/ResponseStatus.cs b/API/Protocol/ResponseStatus.cs index 4c674524f..aa7deb54b 100644 --- a/API/Protocol/ResponseStatus.cs +++ b/API/Protocol/ResponseStatus.cs @@ -1,5 +1,7 @@ namespace GenHTTP.Api.Protocol; +// todo: re-visit + #region Known Types public enum ResponseStatus diff --git a/Adapters/AspNetCore/GenHTTP.Adapters.AspNetCore.csproj b/Adapters/AspNetCore/GenHTTP.Adapters.AspNetCore.csproj index 2b878a728..db5b50013 100644 --- a/Adapters/AspNetCore/GenHTTP.Adapters.AspNetCore.csproj +++ b/Adapters/AspNetCore/GenHTTP.Adapters.AspNetCore.csproj @@ -23,6 +23,7 @@ + @@ -31,8 +32,4 @@ - - - - diff --git a/Directory.Build.props b/Directory.Build.props index 527e8ef2e..853be7dd9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ - net8.0;net9.0;net10.0 + net10.0 14.0 enable @@ -11,7 +11,7 @@ 11.0.0.0 11.0.0.0 - 11.0.0 + 11.0.0-preview.6 Andreas Nägeli @@ -40,7 +40,7 @@ - + diff --git a/Engine/Internal/Context/ClientContext.cs b/Engine/Internal/Context/ClientContext.cs new file mode 100644 index 000000000..533115f1c --- /dev/null +++ b/Engine/Internal/Context/ClientContext.cs @@ -0,0 +1,102 @@ +using System.Buffers; +using System.IO.Pipelines; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; + +using GenHTTP.Api.Infrastructure; + +using GenHTTP.Engine.Internal.Protocol; +using GenHTTP.Engine.Shared.Infrastructure; +using GenHTTP.Engine.Shared.Types; + +namespace GenHTTP.Engine.Internal.Context; + +internal sealed class ClientContext +{ + private static readonly StreamPipeWriterOptions WriterOptions = new(MemoryPool.Shared, leaveOpen: true, minimumBufferSize: 512); + + private IServer? _server; + + private IEndPoint? _endPoint; + + private NetworkConfiguration? _networkConfiguration; + + private Socket? _connection; + + private X509Certificate? _clientCertificate; + + private Stream? _stream; + + private PipeReader? _reader; + + private PipeWriter? _writer; + + private Request _request = new(); + + private ResponseHandler _responseHandler; + + private ClientHandler _clientHandler; + + internal IServer Server => _server ?? throw new InvalidOperationException("Handler has not been initialized"); + + internal IEndPoint EndPoint => _endPoint ?? throw new InvalidOperationException("Handler has not been initialized"); + + internal NetworkConfiguration Configuration => _networkConfiguration ?? throw new InvalidOperationException("Handler has not been initialized"); + + internal Socket Connection => _connection ?? throw new InvalidOperationException("Handler has not been initialized"); + + internal X509Certificate? ClientCertificate => _clientCertificate; + + internal Stream Stream => _stream ?? throw new InvalidOperationException("Handler has not been initialized"); + + internal PipeWriter Writer => _writer ?? throw new InvalidOperationException("Handler has not been initialized"); + + internal PipeReader Reader => _reader ?? throw new InvalidOperationException("Handler has not been initialized"); + + internal Request Request => _request; + + internal ResponseHandler ResponseHandler => _responseHandler; + + internal ClientHandler ClientHandler => _clientHandler; + + internal ClientContext() + { + _clientHandler = new(this); + _responseHandler = new(this); + } + + internal void Apply(Socket socket, Stream stream, PipeReader reader, X509Certificate? clientCertificate, IServer server, IEndPoint endPoint, NetworkConfiguration config) + { + _server = server; + _endPoint = endPoint; + + _connection = socket; + _clientCertificate = clientCertificate; + + _networkConfiguration = config; + + _stream = stream; + + _reader = reader; + _writer = PipeWriter.Create(stream, WriterOptions); + } + + public void Reset() + { + _server = null; + _endPoint = null; + + _connection = null; + _clientCertificate = null; + + _networkConfiguration = null; + + _stream = null; + + _reader = null; + _writer = null; + + Request.Reset(); + } + +} diff --git a/Engine/Internal/Context/ClientContextPolicy.cs b/Engine/Internal/Context/ClientContextPolicy.cs new file mode 100644 index 000000000..d5d143da8 --- /dev/null +++ b/Engine/Internal/Context/ClientContextPolicy.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.ObjectPool; + +namespace GenHTTP.Engine.Internal.Context; + +internal sealed class ClientContextPolicy : PooledObjectPolicy +{ + + public override ClientContext Create() => new(); + + public override bool Return(ClientContext obj) + { + obj.Reset(); + + return true; + } + +} diff --git a/Engine/Internal/GenHTTP.Engine.Internal.csproj b/Engine/Internal/GenHTTP.Engine.Internal.csproj index 30c6b0e6b..3d2c856ce 100644 --- a/Engine/Internal/GenHTTP.Engine.Internal.csproj +++ b/Engine/Internal/GenHTTP.Engine.Internal.csproj @@ -16,10 +16,12 @@ - + - + + + diff --git a/Engine/Internal/Infrastructure/Endpoints/EndPoint.cs b/Engine/Internal/Infrastructure/Endpoints/EndPoint.cs index 2e50b1030..358ba6576 100644 --- a/Engine/Internal/Infrastructure/Endpoints/EndPoint.cs +++ b/Engine/Internal/Infrastructure/Endpoints/EndPoint.cs @@ -1,17 +1,22 @@ -using System.Net; +using System.Buffers; +using System.IO.Pipelines; +using System.Net; using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using GenHTTP.Api.Infrastructure; - -using GenHTTP.Engine.Internal.Protocol; -using GenHTTP.Engine.Internal.Utilities; +using GenHTTP.Engine.Internal.Context; using GenHTTP.Engine.Shared.Infrastructure; +using Microsoft.Extensions.ObjectPool; + namespace GenHTTP.Engine.Internal.Infrastructure.Endpoints; internal abstract class EndPoint : IEndPoint { + private static readonly DefaultObjectPool ContextPool = new(new ClientContextPolicy(), 65536); + + private static readonly StreamPipeReaderOptions ReaderOptions = new(MemoryPool.Shared, leaveOpen: true, bufferSize: 4096 * 4, minimumReadSize: 1024); #region Get-/Setters @@ -109,11 +114,24 @@ private void Handle(Socket client) protected abstract ValueTask Accept(Socket client); - protected ValueTask Handle(Socket client, PoolBufferedStream inputStream, X509Certificate? clientCertificate = null) + protected async ValueTask Handle(Socket client, Stream inputStream, X509Certificate? clientCertificate = null) { client.NoDelay = true; - return new ClientHandler(client, inputStream, clientCertificate, Server, this, Configuration).Run(); + var context = ContextPool.Get(); + + var reader = PipeReader.Create(inputStream, ReaderOptions); + + try + { + context.Apply(client, inputStream, reader, clientCertificate, Server, this, Configuration); + + await context.ClientHandler.RunAsync(); + } + finally + { + ContextPool.Return(context); + } } private static IPAddress DetermineBindingAddress(IPAddress? address, bool dualStack) diff --git a/Engine/Internal/Infrastructure/Endpoints/InsecureEndPoint.cs b/Engine/Internal/Infrastructure/Endpoints/InsecureEndPoint.cs index ba9b1ad23..eff7e4019 100644 --- a/Engine/Internal/Infrastructure/Endpoints/InsecureEndPoint.cs +++ b/Engine/Internal/Infrastructure/Endpoints/InsecureEndPoint.cs @@ -1,15 +1,17 @@ -using System.Net; +using System.Buffers; +using System.IO.Pipelines; +using System.Net; using System.Net.Sockets; using GenHTTP.Api.Infrastructure; -using GenHTTP.Engine.Internal.Utilities; using GenHTTP.Engine.Shared.Infrastructure; namespace GenHTTP.Engine.Internal.Infrastructure.Endpoints; internal sealed class InsecureEndPoint : EndPoint { + private static readonly StreamPipeReaderOptions ReaderOptions = new(MemoryPool.Shared, leaveOpen: true, bufferSize: 65536); #region Initialization @@ -29,7 +31,10 @@ internal InsecureEndPoint(IServer server, IPAddress? address, ushort port, bool #region Functionality - protected override ValueTask Accept(Socket client) => Handle(client, new PoolBufferedStream(new NetworkStream(client), Configuration.TransferBufferSize)); + protected override ValueTask Accept(Socket client) + { + return Handle(client, new NetworkStream(client)); + } #endregion diff --git a/Engine/Internal/Infrastructure/Endpoints/SecureEndPoint.cs b/Engine/Internal/Infrastructure/Endpoints/SecureEndPoint.cs index ccf4f210e..8bc189f30 100644 --- a/Engine/Internal/Infrastructure/Endpoints/SecureEndPoint.cs +++ b/Engine/Internal/Infrastructure/Endpoints/SecureEndPoint.cs @@ -6,14 +6,13 @@ using GenHTTP.Api.Infrastructure; using GenHTTP.Engine.Internal.Protocol; -using GenHTTP.Engine.Internal.Utilities; using GenHTTP.Engine.Shared.Infrastructure; namespace GenHTTP.Engine.Internal.Infrastructure.Endpoints; internal sealed class SecureEndPoint : EndPoint { - + #region Get-/Setters internal SecurityConfiguration Options { get; } @@ -54,7 +53,7 @@ protected override async ValueTask Accept(Socket client) if (stream is not null) { - await Handle(client, new PoolBufferedStream(stream, Configuration.TransferBufferSize), stream.RemoteCertificate); + await Handle(client, stream, stream.RemoteCertificate); } else { diff --git a/Engine/Internal/Infrastructure/Endpoints/SocketPipeReader.cs b/Engine/Internal/Infrastructure/Endpoints/SocketPipeReader.cs new file mode 100644 index 000000000..cf0302cdf --- /dev/null +++ b/Engine/Internal/Infrastructure/Endpoints/SocketPipeReader.cs @@ -0,0 +1,90 @@ +using System.IO.Pipelines; +using System.Net.Sockets; + +namespace GenHTTP.Engine.Internal.Infrastructure.Endpoints; + +public sealed class SocketPipeReader : IAsyncDisposable +{ + private const int MaxOsReceiveSize = 65_536; + + private static readonly PipeOptions PipeOptions = new( + minimumSegmentSize: MaxOsReceiveSize, + pauseWriterThreshold: MaxOsReceiveSize * 2, + resumeWriterThreshold: MaxOsReceiveSize, + useSynchronizationContext: false + ); + + private readonly Socket _socket; + private readonly Pipe _pipe; + private readonly Task _fillTask; + private readonly CancellationTokenSource _cts = new(); + + public PipeReader Reader => _pipe.Reader; + + public SocketPipeReader(Socket socket) + { + _socket = socket ?? throw new ArgumentNullException(nameof(socket)); + _pipe = new Pipe(PipeOptions); + _fillTask = FillPipeAsync(_cts.Token); + } + + private async Task FillPipeAsync(CancellationToken ct) + { + var writer = _pipe.Writer; + + try + { + while (!ct.IsCancellationRequested) + { + var memory = writer.GetMemory(MaxOsReceiveSize); + + var bytesRead = await _socket + .ReceiveAsync(memory, SocketFlags.None, ct) + .ConfigureAwait(false); + + if (bytesRead == 0) + break; + + writer.Advance(bytesRead); + + var result = await writer.FlushAsync(ct).ConfigureAwait(false); + + if (result.IsCompleted || result.IsCanceled) + break; + } + } + catch (OperationCanceledException) + { + /* normal shutdown */ + } + catch (SocketException ex) + { + await writer.CompleteAsync(ex).ConfigureAwait(false); + return; + } + catch (Exception ex) + { + await writer.CompleteAsync(ex).ConfigureAwait(false); + return; + } + + await writer.CompleteAsync().ConfigureAwait(false); + } + + public async ValueTask DisposeAsync() + { + await _cts.CancelAsync(); + + try + { + await _fillTask.ConfigureAwait(false); + } + catch + { + /* already handled in FillPipeAsync */ + } + + await _pipe.Reader.CompleteAsync().ConfigureAwait(false); + _cts.Dispose(); + } +} \ No newline at end of file diff --git a/Engine/Internal/Protocol/ChunkedStream.cs b/Engine/Internal/Protocol/ChunkedStream.cs deleted file mode 100644 index 1fd3d7e78..000000000 --- a/Engine/Internal/Protocol/ChunkedStream.cs +++ /dev/null @@ -1,111 +0,0 @@ -using GenHTTP.Engine.Internal.Utilities; - -namespace GenHTTP.Engine.Internal.Protocol; - -/// -/// Implements chunked transfer encoding by letting the client -/// know how many bytes have been written to the response stream. -/// -/// -/// Response streams are always wrapped into a chunked stream as -/// soon as there is no known content length. To avoid this overhead, -/// specify the length of your content whenever possible. -/// -public sealed class ChunkedStream(PoolBufferedStream target) : Stream -{ - - #region Get-/Setters - - public override bool CanRead => false; - - public override bool CanSeek => false; - - public override bool CanWrite => true; - - public override long Length => throw new NotSupportedException(); - - public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } - - private Stream Target { get; } = target; - - #endregion - - #region Functionality - - public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - if (count > 0) - { - Write(count); - - Target.Write(buffer, offset, count); - - Target.Write("\r\n"u8); - } - } - - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (count > 0) - { - Write(count); - - await Target.WriteAsync(buffer.AsMemory(offset, count), cancellationToken); - - Target.Write("\r\n"u8); - } - } - - public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) - { - if (!buffer.IsEmpty) - { - Write(buffer.Length); - - await Target.WriteAsync(buffer, cancellationToken); - - Target.Write("\r\n"u8); - } - } - - public void Finish() - { - Target.Write("0\r\n\r\n"u8); - } - - public override void Flush() - { - Target.Flush(); - } - - public override Task FlushAsync(CancellationToken cancellationToken) => Target.FlushAsync(cancellationToken); - - private void Write(int value) - { - Span buffer = stackalloc byte[8 + 2]; - - if (value.TryFormat(buffer, out var written, "X")) - { - buffer[written++] = (byte)'\r'; - buffer[written++] = (byte)'\n'; - - Target.Write(buffer[..written]); - } - else - { - throw new InvalidOperationException("Failed to format chunk size"); - } - } - - #endregion - -} diff --git a/Engine/Internal/Protocol/ClientContext.cs b/Engine/Internal/Protocol/ClientContext.cs deleted file mode 100644 index e637f54db..000000000 --- a/Engine/Internal/Protocol/ClientContext.cs +++ /dev/null @@ -1,29 +0,0 @@ -using GenHTTP.Engine.Shared.Types; - -namespace GenHTTP.Engine.Internal.Protocol; - -internal class ClientContext -{ - - internal Request Request { get; } - - internal ResponseBuilder ResponseBuilder { get; } - - internal Response Response { get; } - - internal ClientContext() - { - Response = new Response(); - - ResponseBuilder = new ResponseBuilder(Response); - - Request = new Request(ResponseBuilder); - } - - internal void Reset() - { - Request.Reset(); - Response.Reset(); - } - -} diff --git a/Engine/Internal/Protocol/ClientContextPolicy.cs b/Engine/Internal/Protocol/ClientContextPolicy.cs deleted file mode 100644 index 57e961717..000000000 --- a/Engine/Internal/Protocol/ClientContextPolicy.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.Extensions.ObjectPool; - -namespace GenHTTP.Engine.Internal.Protocol; - -internal class ClientContextPolicy : PooledObjectPolicy -{ - - public override ClientContext Create() => new(); - - public override bool Return(ClientContext obj) - { - obj.Reset(); - return true; - } - -} diff --git a/Engine/Internal/Protocol/ClientHandler.cs b/Engine/Internal/Protocol/ClientHandler.cs index d402db128..c254c3228 100644 --- a/Engine/Internal/Protocol/ClientHandler.cs +++ b/Engine/Internal/Protocol/ClientHandler.cs @@ -1,17 +1,16 @@ using System.Buffers; using System.IO.Pipelines; using System.Net.Sockets; -using System.Security.Cryptography.X509Certificates; +using System.Runtime.CompilerServices; using GenHTTP.Api.Infrastructure; using GenHTTP.Api.Protocol; - -using GenHTTP.Engine.Internal.Protocol.Parser; -using GenHTTP.Engine.Internal.Utilities; -using GenHTTP.Engine.Shared.Infrastructure; +using GenHTTP.Engine.Internal.Context; using GenHTTP.Engine.Shared.Types; -using Microsoft.Extensions.ObjectPool; +using Glyph11; +using Glyph11.Parser.FlexibleParser; +using Glyph11.Protocol; using StringContent = GenHTTP.Modules.IO.Strings.StringContent; @@ -25,179 +24,195 @@ namespace GenHTTP.Engine.Internal.Protocol; /// Implements keep alive and maintains the connection state (e.g. by /// closing it after the last request has been handled). /// -internal sealed class ClientHandler +internal sealed class ClientHandler(ClientContext context) { - private static readonly StreamPipeReaderOptions ReaderOptions = new(MemoryPool.Shared, leaveOpen: true, bufferSize: 65536); - - private static readonly DefaultObjectPool ContextPool = new(new ClientContextPolicy(), 65536); - - #region Get-/Setter - - internal IServer Server { get; } - - internal IEndPoint EndPoint { get; } - - internal NetworkConfiguration Configuration { get; } - - internal Socket Connection { get; } - - internal X509Certificate? ClientCertificate { get; set; } - - internal PoolBufferedStream Stream { get; } - - private ResponseHandler ResponseHandler { get; } - - #endregion - - #region Initialization - - internal ClientHandler(Socket socket, PoolBufferedStream stream, X509Certificate? clientCertificate, IServer server, IEndPoint endPoint, NetworkConfiguration config) - { - Server = server; - EndPoint = endPoint; - - Connection = socket; - ClientCertificate = clientCertificate; - - Configuration = config; + private static readonly TimeSpan InitialReadTimeout = TimeSpan.FromSeconds(10); - Stream = stream; - - ResponseHandler = new ResponseHandler(Server, socket, Stream, Configuration); - } - - #endregion + private static readonly TimeSpan KeepAliveTimeout = TimeSpan.FromSeconds(60); + private CancellationTokenSource _cts = new(); + #region Functionality - internal async ValueTask Run() + internal async ValueTask RunAsync() { + var connection = context.Connection; + try { - await HandlePipe(PipeReader.Create(Stream, ReaderOptions)).ConfigureAwait(false); + await HandlePipeAsync(context.Reader).ConfigureAwait(false); } catch (Exception e) { - Server.Companion?.OnServerError(ServerErrorScope.ClientConnection, Connection.GetAddress(), e); + context.Server.Companion?.OnServerError(ServerErrorScope.ClientConnection, connection.GetAddress(), e); } finally { try { - await Stream.DisposeAsync(); + await context.Stream.DisposeAsync(); } catch (Exception e) { - Server.Companion?.OnServerError(ServerErrorScope.ClientConnection, Connection.GetAddress(), e); + context.Server.Companion?.OnServerError(ServerErrorScope.ClientConnection, connection.GetAddress(), e); } try { - Connection.Shutdown(SocketShutdown.Both); - await Connection.DisconnectAsync(false); - Connection.Close(); + connection.Shutdown(SocketShutdown.Both); + + await connection.DisconnectAsync(false); + + connection.Close(); - Connection.Dispose(); + connection.Dispose(); } catch (Exception e) { - Server.Companion?.OnServerError(ServerErrorScope.ClientConnection, Connection.GetAddress(), e); + context.Server.Companion?.OnServerError(ServerErrorScope.ClientConnection, connection.GetAddress(), e); } } } - private async ValueTask HandlePipe(PipeReader reader) + private async ValueTask HandlePipeAsync(PipeReader reader) { - var context = ContextPool.Get(); + ResetCts(InitialReadTimeout); + + var request = context.Request; + var into = request.Source; try { - using var buffer = new RequestBuffer(reader, Configuration); - - var parser = new RequestParser(Configuration, context.Request); - - try + while (true) { - var firstRequest = true; + ReadResult result; - while (Server.Running) + try { - if (firstRequest) - { - firstRequest = false; - } - else - { - context.Reset(); - } + result = await reader.ReadAsync(_cts.Token); + } + catch (OperationCanceledException) + { + return; + } + catch (IOException e) when (e.InnerException is SocketException { SocketErrorCode: SocketError.ConnectionReset or SocketError.ConnectionAborted }) + { + return; + } + catch (IOException e) when (e.Message.Contains("Broken pipe", StringComparison.OrdinalIgnoreCase)) + { + return; + } - if (!await parser.TryParseAsync(buffer)) - { - break; - } + var buffer = result.Buffer; - var status = await HandleRequest(context.Request, !buffer.ReadRequired); + var handledRequest = false; + + while (TryParseRequest(ref buffer, into)) + { + request.Apply(); - if (status is Api.Protocol.Connection.Close) + var status = await HandleRequestAsync(request); + + if (status is Connection.Close) { return; } + + request.Reset(); + + handledRequest = true; } + + reader.AdvanceTo(buffer.Start, buffer.End); + + if (!handledRequest) break; + + await context.Writer.FlushAsync(); + + if (result.IsCompleted) break; + + ResetCts(KeepAliveTimeout); } - catch (ProtocolException pe) - { - // client did something wrong - await SendError(pe, ResponseStatus.BadRequest); - throw; - } - catch (Exception e) - { - // we did something wrong - await SendError(e, ResponseStatus.InternalServerError); - throw; - } + } + catch (HttpParseException pe) + { + // client did something wrong + await SendErrorAsync(pe, ResponseStatus.BadRequest); + throw; + } + catch (Exception e) + { + // we did something wrong + await SendErrorAsync(e, ResponseStatus.InternalServerError); + throw; } finally { - ContextPool.Return(context); - await reader.CompleteAsync(); } } - private async ValueTask HandleRequest(Request request, bool dataRemaining) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryParseRequest(ref ReadOnlySequence buffer, BinaryRequest into) { - request.SetConnection(Server, EndPoint, Connection.GetAddress(), ClientCertificate); + if (!FlexibleParser.TryExtractFullHeader(ref buffer, into, out var bytesRead)) + { + return false; + } + + buffer = buffer.Slice(bytesRead); + return true; + } - var keepAliveRequested = request["Connection"]?.Equals("Keep-Alive", StringComparison.InvariantCultureIgnoreCase) ?? request.ProtocolType == HttpProtocol.Http11; + private async ValueTask HandleRequestAsync(Request request) + { + // request.SetConnection(Server, EndPoint, Connection.GetAddress(), ClientCertificate); - var response = await Server.Handler.HandleAsync(request) ?? throw new InvalidOperationException("The root request handler did not return a response"); + var keepAliveRequested = true; // request["Connection"]?.Equals("Keep-Alive", StringComparison.InvariantCultureIgnoreCase) ?? request.ProtocolType == HttpProtocol.Http11; - var closeRequested = response.Connection is Api.Protocol.Connection.Close or Api.Protocol.Connection.Upgrade; + var response = await context.Server.Handler.HandleAsync(request) ?? throw new InvalidOperationException("The root request handler did not return a response"); - var active = await ResponseHandler.Handle(request, response, request.ProtocolType, keepAliveRequested && !closeRequested, dataRemaining); + var closeRequested = false; // response.Connection is Api.Protocol.Connection.Close or Api.Protocol.Connection.Upgrade; - return (active && keepAliveRequested && !closeRequested) ? Api.Protocol.Connection.KeepAlive : Api.Protocol.Connection.Close; + var active = await context.ResponseHandler.HandleAsync(request, response, HttpProtocol.Http11, keepAliveRequested && !closeRequested); + + return (active && keepAliveRequested && !closeRequested) ? Connection.KeepAlive : Connection.Close; } - private async ValueTask SendError(Exception e, ResponseStatus status) + private ValueTask SendErrorAsync(Exception e, ResponseStatus status) { try { - var message = Server.Development ? e.ToString() : e.Message; + var message = context.Server.Development ? e.ToString() : e.Message; - using var response = new ResponseBuilder(new()).Status(status) - .Content(new StringContent(message)) - .Build(); + // todo status code mapping - await ResponseHandler.Handle(null, response, HttpProtocol.Http10, false, false); + var response = new ResponseBuilder().Raw() + .Status(status) + .Content(new StringContent(message)) + .Build(); + + return context.ResponseHandler.HandleAsync(null, response, HttpProtocol.Http10, false); } catch { /* no recovery here */ + return ValueTask.FromResult(false); } } - #endregion + private void ResetCts(TimeSpan timeout) + { + if (!_cts.TryReset()) + { + _cts.Dispose(); + _cts = new CancellationTokenSource(); + } + + _cts.CancelAfter(timeout); + } -} + #endregion + +} \ No newline at end of file diff --git a/Engine/Internal/Protocol/DateHeader.cs b/Engine/Internal/Protocol/DateHeader.cs index ceccbfe37..a38388262 100644 --- a/Engine/Internal/Protocol/DateHeader.cs +++ b/Engine/Internal/Protocol/DateHeader.cs @@ -1,4 +1,6 @@ -namespace GenHTTP.Engine.Internal.Protocol; +using System.Runtime.CompilerServices; + +namespace GenHTTP.Engine.Internal.Protocol; /// /// Caches the value of the date header for one second @@ -6,27 +8,28 @@ /// public static class DateHeader { - private static string _value = string.Empty; - - private static byte _second = 61; + private static readonly byte[] Buffer = new byte[6 + 29 + 2]; // "Date: " + RFC1123 + "\r\n" + + private static readonly ReadOnlyMemory Value = Buffer; - #region Functionality + private static int _second = 61; - public static string GetValue() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory GetValue() { var now = DateTime.UtcNow; - var second = now.Second; - if (second != _second) - { - _second = (byte)second; - _value = now.ToString("r"); - } + if (second == _second) return Value; - return _value; - } - - #endregion + _second = second; + + "Date: "u8.CopyTo(Buffer); + + now.TryFormat(Buffer.AsSpan(6), out _, "r"); + + "\r\n"u8.CopyTo(Buffer.AsSpan(35)); -} + return Value; + } +} \ No newline at end of file diff --git a/Engine/Internal/Protocol/Parser/ChunkedContentParser.cs b/Engine/Internal/Protocol/Parser/ChunkedContentParser.cs deleted file mode 100644 index f4ab0a0ab..000000000 --- a/Engine/Internal/Protocol/Parser/ChunkedContentParser.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Buffers; -using System.Globalization; -using GenHTTP.Engine.Internal.Protocol.Parser.Conversion; -using GenHTTP.Engine.Shared.Infrastructure; - -namespace GenHTTP.Engine.Internal.Protocol.Parser; - -/// -/// Reads the chunked encoded body of a client request into -/// a stream. -/// -/// -/// As we cannot know the length of the request beforehand, -/// this will always use a file stream for buffering. -/// -internal sealed class ChunkedContentParser -{ - - #region Initialization - - internal ChunkedContentParser(NetworkConfiguration networkConfiguration) - { - Configuration = networkConfiguration; - } - - #endregion - - #region Get-/Setters - - private NetworkConfiguration Configuration { get; } - - #endregion - - #region Functionality - - internal async ValueTask GetBody(RequestBuffer buffer) - { - var body = TemporaryFileStream.Create(); - - var bufferSize = Configuration.TransferBufferSize; - - while (await NextChunkAsync(buffer, body, bufferSize)) { } - - body.Seek(0, SeekOrigin.Begin); - - return body; - } - - private static async ValueTask NextChunkAsync(RequestBuffer buffer, Stream target, uint bufferSize) - { - await EnsureDataAsync(buffer); - - // - // chunks are of the following form: - // - // ABC - // - // - // - // with the final chunk having a size of 0 - // - var chunkSize = GetChunkSize(buffer); - - if (chunkSize == 0) - { - return false; - } - await RequestContentParser.CopyAsync(buffer, target, chunkSize, bufferSize); - - buffer.Advance(2); - - return true; - } - - private static long GetChunkSize(RequestBuffer buffer) - { - var reader = new SequenceReader(buffer.Data); - - if (reader.IsNext((byte)'0')) - { - buffer.Advance(5); - return 0; - } - if (reader.TryReadTo(out ReadOnlySequence lengthInHex, (byte)'\r')) - { - var hexString = ValueConverter.GetString(lengthInHex); - - if (long.TryParse(hexString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var length)) - { - buffer.Advance(lengthInHex.Length + 2); - - return length; - } - - throw new ProtocolException("Invalid chunk size format"); - } - - throw new ProtocolException("Chunk size expected"); - } - - private static async ValueTask EnsureDataAsync(RequestBuffer buffer) - { - if (buffer.ReadRequired) - { - if (await buffer.ReadAsync() == null) - { - throw new ProtocolException("Timeout while waiting for client data"); - } - } - } - - #endregion - -} diff --git a/Engine/Internal/Protocol/Parser/Conversion/HeaderConverter.cs b/Engine/Internal/Protocol/Parser/Conversion/HeaderConverter.cs deleted file mode 100644 index e84e45966..000000000 --- a/Engine/Internal/Protocol/Parser/Conversion/HeaderConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Buffers; - -namespace GenHTTP.Engine.Internal.Protocol.Parser.Conversion; - -internal static class HeaderConverter -{ - private static readonly string[] KnownHeaders = ["Host", "User-Agent", "Accept", "Content-Type", "Content-Length"]; - - private static readonly string[] KnownValues = ["*/*"]; - - internal static string ToKey(ReadOnlySequence value) - { - foreach (var known in KnownHeaders) - { - if (ValueConverter.CompareTo(value, known)) - { - return known; - } - } - - return ValueConverter.GetString(value); - } - - internal static string ToValue(ReadOnlySequence value) - { - foreach (var known in KnownValues) - { - if (ValueConverter.CompareTo(value, known)) - { - return known; - } - } - - return ValueConverter.GetString(value); - } -} diff --git a/Engine/Internal/Protocol/Parser/Conversion/MethodConverter.cs b/Engine/Internal/Protocol/Parser/Conversion/MethodConverter.cs deleted file mode 100644 index b9b717689..000000000 --- a/Engine/Internal/Protocol/Parser/Conversion/MethodConverter.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Buffers; -using GenHTTP.Api.Protocol; - -namespace GenHTTP.Engine.Internal.Protocol.Parser.Conversion; - -internal static class MethodConverter -{ - private static readonly Dictionary KnownMethods = new(7) - { - { - "GET", RequestMethod.Get - }, - { - "HEAD", RequestMethod.Head - }, - { - "POST", RequestMethod.Post - }, - { - "PUT", RequestMethod.Put - }, - { - "PATCH", RequestMethod.Patch - }, - { - "DELETE", RequestMethod.Delete - }, - { - "OPTIONS", RequestMethod.Options - } - }; - - internal static FlexibleRequestMethod ToRequestMethod(ReadOnlySequence value) - { - foreach (var kv in KnownMethods) - { - if (ValueConverter.CompareTo(value, kv.Key)) - { - return FlexibleRequestMethod.Get(kv.Value); - } - } - - return FlexibleRequestMethod.Get(ValueConverter.GetString(value)); - } -} diff --git a/Engine/Internal/Protocol/Parser/Conversion/PathConverter.cs b/Engine/Internal/Protocol/Parser/Conversion/PathConverter.cs deleted file mode 100644 index a9bd5ce8b..000000000 --- a/Engine/Internal/Protocol/Parser/Conversion/PathConverter.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Buffers; -using GenHTTP.Api.Routing; - -namespace GenHTTP.Engine.Internal.Protocol.Parser.Conversion; - -internal static class PathConverter -{ - private static readonly WebPath Root = new(new List(), true); - - internal static WebPath ToPath(ReadOnlySequence value) - { - if (value.Length == 1) - { - return Root; - } - - var reader = new SequenceReader(value); - - reader.Advance(1); - - var parts = new List(4); - - while (reader.TryReadTo(out ReadOnlySequence segment, (byte)'/')) - { - parts.Add(new WebPathPart(ValueConverter.GetString(segment))); - } - - if (!reader.End) - { - var remainder = reader.Sequence.Slice(reader.Position); - parts.Add(new WebPathPart(ValueConverter.GetString(remainder))); - - return new WebPath(parts, false); - } - - return new WebPath(parts, true); - } -} diff --git a/Engine/Internal/Protocol/Parser/Conversion/ProtocolConverter.cs b/Engine/Internal/Protocol/Parser/Conversion/ProtocolConverter.cs deleted file mode 100644 index 91eff3d11..000000000 --- a/Engine/Internal/Protocol/Parser/Conversion/ProtocolConverter.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Buffers; -using GenHTTP.Api.Protocol; - -namespace GenHTTP.Engine.Internal.Protocol.Parser.Conversion; - -internal static class ProtocolConverter -{ - - internal static HttpProtocol ToProtocol(ReadOnlySequence value) - { - var reader = new SequenceReader(value); - - if (value.Length != 8) - { - throw new ProtocolException($"HTTP protocol version expected (got: '{ValueConverter.GetString(value)}')"); - } - - reader.Advance(5); - - var version = reader.Sequence.Slice(reader.Position); - - if (ValueConverter.CompareTo(version, "1.1")) - { - return HttpProtocol.Http11; - } - if (ValueConverter.CompareTo(version, "1.0")) - { - return HttpProtocol.Http10; - } - - var versionString = ValueConverter.GetString(version); - - throw new ProtocolException($"Unexpected protocol version '{versionString}'"); - } -} diff --git a/Engine/Internal/Protocol/Parser/Conversion/QueryConverter.cs b/Engine/Internal/Protocol/Parser/Conversion/QueryConverter.cs deleted file mode 100644 index bde1a3b99..000000000 --- a/Engine/Internal/Protocol/Parser/Conversion/QueryConverter.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Buffers; - -namespace GenHTTP.Engine.Internal.Protocol.Parser.Conversion; - -internal static class QueryConverter -{ - - internal static void Emit(Request request, ReadOnlySequence value) - { - if (!value.IsEmpty) - { - var reader = new SequenceReader(value); - - while (reader.TryReadTo(out ReadOnlySequence segment, (byte)'&')) - { - EmitSegment(request, segment); - } - - if (!reader.End) - { - var remainder = reader.Sequence.Slice(reader.Position); - EmitSegment(request, remainder); - } - } - } - - private static void EmitSegment(Request request, ReadOnlySequence segment) - { - if (!segment.IsEmpty) - { - var reader = new SequenceReader(segment); - - string? name, value = null; - - if (reader.TryReadTo(out ReadOnlySequence firstSegment, (byte)'=')) - { - name = ValueConverter.GetString(firstSegment); - - if (!reader.End) - { - var remainingValue = reader.Sequence.Slice(reader.Position); - value = ValueConverter.GetString(remainingValue); - } - } - else - { - var remainingName = reader.Sequence.Slice(reader.Position); - name = ValueConverter.GetString(remainingName); - } - - request.SetQuery(Uri.UnescapeDataString(name), value != null ? Uri.UnescapeDataString(value) : string.Empty); - } - } - -} diff --git a/Engine/Internal/Protocol/Parser/Conversion/ValueConverter.cs b/Engine/Internal/Protocol/Parser/Conversion/ValueConverter.cs deleted file mode 100644 index 2707b169c..000000000 --- a/Engine/Internal/Protocol/Parser/Conversion/ValueConverter.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Buffers; -using System.Text; - -namespace GenHTTP.Engine.Internal.Protocol.Parser.Conversion; - -internal static class ValueConverter -{ - private static readonly Encoding Ascii = Encoding.ASCII; - - internal static bool CompareTo(ReadOnlySequence buffer, string expected) - { - var i = 0; - - if (buffer.Length != expected.Length) - { - return false; - } - - foreach (var segment in buffer) - { - for (var j = 0; j < segment.Length; j++) - { - if (segment.Span[j] != expected[i++]) - { - return false; - } - } - } - - return true; - } - - internal static string GetString(ReadOnlySequence buffer) - { - if (buffer.Length > 0) - { - var result = string.Create((int)buffer.Length, buffer, (span, sequence) => - { - foreach (var segment in sequence) - { - Ascii.GetChars(segment.Span, span); - span = span[segment.Length..]; - } - }); - - return result.Trim(); - } - - return string.Empty; - } -} diff --git a/Engine/Internal/Protocol/Parser/RequestContentParser.cs b/Engine/Internal/Protocol/Parser/RequestContentParser.cs deleted file mode 100644 index 756e2c649..000000000 --- a/Engine/Internal/Protocol/Parser/RequestContentParser.cs +++ /dev/null @@ -1,75 +0,0 @@ -using GenHTTP.Engine.Shared.Infrastructure; - -namespace GenHTTP.Engine.Internal.Protocol.Parser; - -/// -/// Efficiently reads the body from the HTTP request, storing it -/// in a temporary file if it exceeds the buffering limits. -/// -internal sealed class RequestContentParser -{ - - #region Initialization - - internal RequestContentParser(long length, NetworkConfiguration configuration) - { - Length = length; - Configuration = configuration; - } - - #endregion - - #region Get-/Setters - - internal long Length { get; } - - internal NetworkConfiguration Configuration { get; } - - #endregion - - #region Functionality - - internal async Task GetBody(RequestBuffer buffer) - { - var body = Length > Configuration.RequestMemoryLimit ? TemporaryFileStream.Create() : new MemoryStream((int)Length); - - await CopyAsync(buffer, body, Length, Configuration.TransferBufferSize); - - body.Seek(0, SeekOrigin.Begin); - - return body; - } - - internal static async ValueTask CopyAsync(RequestBuffer source, Stream target, long length, uint bufferSize) - { - var toFetch = length; - - while (toFetch > 0) - { - await source.ReadAsync(); - - var toRead = Math.Min(source.Data.Length, Math.Min(bufferSize, toFetch)); - - if (toRead == 0) - { - throw new InvalidOperationException($"No data read from the transport but {toFetch} bytes are remaining"); - } - - var data = source.Data.Slice(0, toRead); - - var position = data.GetPosition(0); - - while (data.TryGet(ref position, out var memory)) - { - await target.WriteAsync(memory); - } - - source.Advance(toRead); - - toFetch -= toRead; - } - } - - #endregion - -} diff --git a/Engine/Internal/Protocol/Parser/RequestParser.cs b/Engine/Internal/Protocol/Parser/RequestParser.cs deleted file mode 100644 index b603647be..000000000 --- a/Engine/Internal/Protocol/Parser/RequestParser.cs +++ /dev/null @@ -1,195 +0,0 @@ -using System.Globalization; -using GenHTTP.Engine.Internal.Protocol.Parser.Conversion; -using GenHTTP.Engine.Shared.Infrastructure; - -namespace GenHTTP.Engine.Internal.Protocol.Parser; - -/// -/// Reads the next HTTP request to be handled by the server from -/// the client connection. -/// -/// -/// Be aware that this code path is heavily optimized for low -/// memory allocations. Changes to this class should allocate -/// as few memory as possible to avoid the performance of -/// the server from being impacted in a negative manner. -/// -internal sealed class RequestParser -{ - - #region Initialization - - internal RequestParser(NetworkConfiguration configuration, Request request) - { - Configuration = configuration; - Request = request; - - Scanner = new RequestScanner(); - } - - #endregion - - #region Get-/Setters - - private NetworkConfiguration Configuration { get; } - - private RequestScanner Scanner { get; } - - private Request Request { get; } - - #endregion - - #region Functionality - - internal async ValueTask TryParseAsync(RequestBuffer buffer) - { - if (!await Type(buffer)) - { - if (!buffer.Data.IsEmpty) - { - throw new ProtocolException("Unable to read HTTP verb from request line."); - } - - return false; - } - - await Path(buffer); - - await Protocol(buffer); - - await Headers(buffer); - - await Body(buffer); - - RequestSecurity.Validate(Request); - - return true; - } - - private async ValueTask Type(RequestBuffer buffer) - { - Scanner.Mode = ScannerMode.Words; - - if (await Scanner.Next(buffer, RequestToken.Word, true)) - { - Request.SetMethod(MethodConverter.ToRequestMethod(Scanner.Value)); - return true; - } - - return false; - } - - private async ValueTask Path(RequestBuffer buffer) - { - Scanner.Mode = ScannerMode.Path; - - var token = await Scanner.Next(buffer); - - // path - if (token == RequestToken.Path) - { - Request.SetPath(PathConverter.ToPath(Scanner.Value)); - } - else if (token == RequestToken.PathWithQuery) - { - Request.SetPath(PathConverter.ToPath(Scanner.Value)); - - // query - Scanner.Mode = ScannerMode.Words; - - if (await Scanner.Next(buffer, RequestToken.Word, includeWhitespace: true)) - { - QueryConverter.Emit(Request, Scanner.Value); - } - } - else - { - throw new ProtocolException($"Unexpected token while parsing path: {token}"); - } - } - - private async ValueTask Protocol(RequestBuffer buffer) - { - Scanner.Mode = ScannerMode.Words; - - if (await Scanner.Next(buffer, RequestToken.Word)) - { - Request.SetProtocol(ProtocolConverter.ToProtocol(Scanner.Value)); - } - } - - private async ValueTask Headers(RequestBuffer buffer) - { - Scanner.Mode = ScannerMode.HeaderKey; - - while (await Scanner.Next(buffer) == RequestToken.Word) - { - var key = HeaderConverter.ToKey(Scanner.Value); - - Scanner.Mode = ScannerMode.HeaderValue; - - if (await Scanner.Next(buffer, RequestToken.Word)) - { - Request.SetHeader(key, HeaderConverter.ToValue(Scanner.Value)); - } - - Scanner.Mode = ScannerMode.HeaderKey; - } - } - - private async ValueTask Body(RequestBuffer buffer) - { - var headers = Request.Headers; - - var contentLength = headers.GetValueOrDefault("Content-Length"); - var transferEncoding = headers.GetValueOrDefault("Transfer-Encoding"); - - if (contentLength != null && transferEncoding != null) - { - throw new ProtocolException("Both 'Content-Length' and 'Transfer-Encoding' have been specified"); - } - - if (contentLength != null) - { - if (Request.ContainsMultipleHeaders("Content-Length")) - { - throw new ProtocolException("Multiple 'Content-Length' headers specified."); - } - - if (long.TryParse(contentLength, NumberStyles.None, CultureInfo.InvariantCulture, out var length)) - { - if (length > 0) - { - var parser = new RequestContentParser(length, Configuration); - - Request.SetContent(await parser.GetBody(buffer)); - } - } - else - { - throw new ProtocolException("Unable to parse the given 'Content-Length' header"); - } - } - else if (transferEncoding != null) - { - if (Request.ContainsMultipleHeaders("Transfer-Encoding")) - { - throw new ProtocolException("Multiple 'Transfer-Encoding' headers specified."); - } - - if (string.Compare(transferEncoding, "chunked", StringComparison.OrdinalIgnoreCase) == 0) - { - var parser = new ChunkedContentParser(Configuration); - - Request.SetContent(await parser.GetBody(buffer)); - } - else - { - throw new ProtocolException("Only transfer encoding mode 'chunked' is allowed by this endpoint"); - } - } - } - - #endregion - -} diff --git a/Engine/Internal/Protocol/Parser/RequestScanner.cs b/Engine/Internal/Protocol/Parser/RequestScanner.cs deleted file mode 100644 index d4f930b02..000000000 --- a/Engine/Internal/Protocol/Parser/RequestScanner.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System.Buffers; - -namespace GenHTTP.Engine.Internal.Protocol.Parser; - -internal sealed class RequestScanner -{ - - internal RequestScanner() - { - Current = RequestToken.None; - Mode = ScannerMode.Words; - } - - internal RequestToken Current { get; private set; } - - internal ReadOnlySequence Value { get; private set; } - - internal ScannerMode Mode { get; set; } - - internal async ValueTask Next(RequestBuffer buffer, RequestToken expectedToken, bool allowNone = false, bool includeWhitespace = false) - { - var read = await Next(buffer, false, includeWhitespace); - - if (allowNone && read == RequestToken.None) - { - return false; - } - - if (read != expectedToken) - { - throw new ProtocolException($"Unexpected token '{read}' (expected '{expectedToken}')"); - } - - return true; - } - - internal async ValueTask Next(RequestBuffer buffer, bool forceRead = false, bool includeWhitespace = false) - { - // ensure we have data to be scanned - if (await Fill(buffer, forceRead)) - { - var found = ScanData(buffer, includeWhitespace); - - if (found != null) - { - return Current = found.Value; - } - } - - // did not recognize any tokens, probably due to missing input data - if (!forceRead && !buffer.Timeout) - { - return await Next(buffer, true, includeWhitespace); - } - - return Current = RequestToken.None; - } - - private RequestToken? ScanData(RequestBuffer buffer, bool includeWhitespace = false) - { - if (SkipWhitespace(buffer) && includeWhitespace) - { - Value = new ReadOnlySequence(); - return RequestToken.Word; - } - - if (Mode == ScannerMode.Words) - { - if (ReadTo(buffer, ' ', '\r')) - { - return RequestToken.Word; - } - if (ReadTo(buffer, '\r', skipAdditionally: 1)) - { - return RequestToken.Word; - } - } - else if (Mode == ScannerMode.Path) - { - if (ReadTo(buffer, '?', '\r')) - { - return RequestToken.PathWithQuery; - } - if (ReadTo(buffer, ' ', '\r')) - { - return RequestToken.Path; - } - } - else if (Mode == ScannerMode.HeaderKey) - { - if (IsNewLine(buffer)) - { - return RequestToken.NewLine; - } - if (ReadTo(buffer, ':', '\r', 1)) - { - return RequestToken.Word; - } - } - else if (Mode == ScannerMode.HeaderValue) - { - if (ReadTo(buffer, '\r', skipAdditionally: 1)) - { - return RequestToken.Word; - } - } - - return null; - } - - private static bool SkipWhitespace(RequestBuffer buffer) - { - var count = 0; - var done = false; - - foreach (var memory in buffer.Data) - { - for (var i = 0; i < memory.Length; i++) - { - if (memory.Span[i] == (byte)' ') - { - count++; - } - else - { - done = true; - break; - } - } - - if (done) - { - break; - } - } - - if (count > 0) - { - buffer.Advance(count); - } - - return count > 0; - } - - private static bool IsNewLine(RequestBuffer buffer) - { - if (buffer.Data.FirstSpan[0] == (byte)'\r') - { - buffer.Advance(2); - return true; - } - - return false; - } - - private bool ReadTo(RequestBuffer buffer, char delimiter, char? boundary = null, byte skipAdditionally = 0) - { - var reader = new SequenceReader(buffer.Data); - - if (reader.TryReadTo(out ReadOnlySequence value, (byte)delimiter)) - { - if (boundary != null) - { - var boundaryReader = new SequenceReader(buffer.Data); - - if (boundaryReader.TryReadTo(out ReadOnlySequence boundaryData, (byte)boundary)) - { - if (boundaryData.Length < value.Length) - { - return false; - } - } - } - - Value = value; - buffer.Advance(value.Length + 1 + skipAdditionally); - - return true; - } - - return false; - } - - private static async ValueTask Fill(RequestBuffer buffer, bool force = false) - { - if (buffer.ReadRequired || force) - { - await buffer.ReadAsync(force); - } - - return !buffer.Data.IsEmpty; - } - -} diff --git a/Engine/Internal/Protocol/Parser/RequestSecurity.cs b/Engine/Internal/Protocol/Parser/RequestSecurity.cs deleted file mode 100644 index 3ac8994be..000000000 --- a/Engine/Internal/Protocol/Parser/RequestSecurity.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace GenHTTP.Engine.Internal.Protocol.Parser; - -internal static class RequestSecurity -{ - - public static void Validate(Request request) - { - if (!request.Headers.ContainsKey("Host")) - { - throw new ProtocolException("Mandatory 'Host' header is missing from the request"); - } - - if (request.ContainsMultipleHeaders("Host")) - { - throw new ProtocolException("Multiple 'Host' headers specified"); - } - - var target = request.Target.Path.Parts; - - for (var i = 0; i < target.Count; i++) - { - if (target[i].Value == "." || target[i].Value == "..") - { - throw new ProtocolException("Segments '.' or '..' are now allowed in path"); - } - } - } - -} diff --git a/Engine/Internal/Protocol/Parser/RequestToken.cs b/Engine/Internal/Protocol/Parser/RequestToken.cs deleted file mode 100644 index 512d2f0c9..000000000 --- a/Engine/Internal/Protocol/Parser/RequestToken.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace GenHTTP.Engine.Internal.Protocol.Parser; - -internal enum RequestToken -{ - None, - Word, - Path, - PathWithQuery, - NewLine -} diff --git a/Engine/Internal/Protocol/Parser/ScannerMode.cs b/Engine/Internal/Protocol/Parser/ScannerMode.cs deleted file mode 100644 index 3e8b401c6..000000000 --- a/Engine/Internal/Protocol/Parser/ScannerMode.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace GenHTTP.Engine.Internal.Protocol.Parser; - -internal enum ScannerMode -{ - Words, - Path, - HeaderKey, - HeaderValue -} diff --git a/Engine/Internal/Protocol/ProtocolException.cs b/Engine/Internal/Protocol/ProtocolException.cs deleted file mode 100644 index 6e143995f..000000000 --- a/Engine/Internal/Protocol/ProtocolException.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace GenHTTP.Engine.Internal.Protocol; - -/// -/// Thrown by the server, if the HTTP protocol has -/// somehow been violated (either by the server or the client). -/// -[Serializable] -public class ProtocolException : Exception -{ - - public ProtocolException(string reason) : base(reason) - { - - } - -} diff --git a/Engine/Internal/Protocol/Request.cs b/Engine/Internal/Protocol/Request.cs deleted file mode 100644 index 9af7d5b69..000000000 --- a/Engine/Internal/Protocol/Request.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System.Net; -using System.Security.Cryptography.X509Certificates; - -using GenHTTP.Api.Infrastructure; -using GenHTTP.Api.Protocol; -using GenHTTP.Api.Routing; -using GenHTTP.Engine.Shared.Types; - -using CookieCollection = GenHTTP.Engine.Shared.Types.CookieCollection; - -namespace GenHTTP.Engine.Internal.Protocol; - -/// -/// Provides methods to access a recieved http request. -/// -internal sealed class Request : IRequest -{ - private readonly ResponseBuilder _responseBuilder; - - private bool _freshResponse = true; - - private IServer? _server; - private IEndPoint? _endPoint; - - private IClientConnection? _clientConnection; - private IClientConnection? _localClient; - - private FlexibleRequestMethod? _method; - private RoutingTarget? _target; - - private readonly RequestHeaderCollection _headers = new(); - - private readonly CookieCollection _cookies = new(); - - private readonly ForwardingCollection _forwardings = new(); - - private readonly RequestProperties _properties = new(); - - private readonly RequestQuery _query = new(); - - private Stream? _content; - private FlexibleContentType? _contentType; - - #region Initialization - - internal Request(ResponseBuilder responseBuilder) - { - _responseBuilder = responseBuilder; - } - - #endregion - - #region Get-/Setters - - public IServer Server => _server ?? throw new InvalidOperationException("Request is not initialized yet"); - - public IEndPoint EndPoint => _endPoint ?? throw new InvalidOperationException("Request is not initialized yet"); - - public IClientConnection Client => _clientConnection ?? throw new InvalidOperationException("Request is not initialized yet"); - - public IClientConnection LocalClient => _localClient ?? throw new InvalidOperationException("Request is not initialized yet"); - - public HttpProtocol ProtocolType { get; internal set; } - - public FlexibleRequestMethod Method => _method ?? throw new InvalidOperationException("Request is not initialized yet"); - - public RoutingTarget Target => _target ?? throw new InvalidOperationException("Request is not initialized yet"); - - public IHeaderCollection Headers => _headers; - - public Stream? Content => _content; - - public FlexibleContentType? ContentType - { - get - { - if (_contentType is not null) - { - return _contentType; - } - - var type = this["Content-Type"]; - - if (type is not null) - { - return _contentType = new FlexibleContentType(type); - } - - return null; - } - } - - public string? Host => Client.Host; - - public string? Referer => this["Referer"]; - - public string? UserAgent => this["User-Agent"]; - - public string? this[string additionalHeader] => Headers.GetValueOrDefault(additionalHeader); - - public ICookieCollection Cookies => _cookies; - - public IForwardingCollection Forwardings => _forwardings; - - public IRequestQuery Query => _query; - - public IRequestProperties Properties => _properties; - - #endregion - - #region Functionality - - public IResponseBuilder Respond() - { - if (!_freshResponse) - { - _responseBuilder.Reset(); - } - else - { - _freshResponse = false; - } - - return _responseBuilder; - } - - public bool ContainsMultipleHeaders(string key) => _headers.ContainsMultiple(key); - - #endregion - - #region Parsing - - internal void SetConnection(IServer server, IEndPoint endPoint, IPAddress? address, X509Certificate? clientCertificate) - { - _server = server; - _endPoint = endPoint; - - var protocol = _endPoint.Secure ? ClientProtocol.Https : ClientProtocol.Http; - - if (_forwardings.Count == 0) - { - _forwardings.TryAddLegacy(Headers); - } - - _localClient = new ClientConnection(address, protocol, Headers["Host"], clientCertificate); - - _clientConnection = _forwardings.DetermineClient(clientCertificate) ?? _localClient; - } - - internal void SetHeader(string key, string value) - { - if (string.Equals(key, "cookie", StringComparison.OrdinalIgnoreCase)) - { - _cookies.Add(value); - } - else if (string.Equals(key, "forwarded", StringComparison.OrdinalIgnoreCase)) - { - _forwardings.Add(value); - } - else - { - _headers.Add(key, value); - } - } - - internal void SetContent(Stream content) - { - _content = content; - } - - internal void SetProtocol(HttpProtocol protocol) - { - ProtocolType = protocol; - } - - internal void SetPath(WebPath path) - { - _target = new(path); - } - - internal void SetMethod(FlexibleRequestMethod method) - { - _method = method; - } - - internal void SetQuery(string key, string value) - { - _query[key] = value; - } - - internal void Reset() - { - _headers.Clear(); - _cookies.Clear(); - _forwardings.Clear(); - _properties.Clear(); - _query.Clear(); - - Content?.Dispose(); - - _content = null; - _contentType = null; - - _freshResponse = true; - } - - #endregion - - #region IDisposable Support - - private bool _disposed; - - public void Dispose() - { - if (!_disposed) - { - Content?.Dispose(); - - _disposed = true; - } - } - - #endregion - -} diff --git a/Engine/Internal/Protocol/RequestBuffer.cs b/Engine/Internal/Protocol/RequestBuffer.cs deleted file mode 100644 index 54011f07e..000000000 --- a/Engine/Internal/Protocol/RequestBuffer.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System.Buffers; -using System.IO.Pipelines; - -using GenHTTP.Engine.Shared.Infrastructure; - -namespace GenHTTP.Engine.Internal.Protocol; - -/// -/// Buffers the data received from a client, converting it into a contiguous chunk of data, -/// therefore making it easier for the parser engine to be processed. -/// -/// -/// Depending on how fast a client is able to upload request data, -/// the server may need to wait for the whole request to be available. -/// Additionally, keep alive connections will be held open by the client -/// until there is a new request to be sent. The buffer implements request -/// read timeouts by continuously reading data from the underlying network -/// stream. If the read operation times out, the server will close the -/// connection. -/// -internal sealed class RequestBuffer : IDisposable -{ - private ReadOnlySequence? _data; - - #region Initialization - - internal RequestBuffer(PipeReader reader, NetworkConfiguration configuration) - { - Reader = reader; - Configuration = configuration; - } - - #endregion - - #region Get-/Setters - - private PipeReader Reader { get; } - - private NetworkConfiguration Configuration { get; } - - private CancellationTokenSource? Cancellation { get; set; } - - internal ReadOnlySequence Data => _data ?? new ReadOnlySequence(); - - internal bool ReadRequired => _data == null || _data.Value.IsEmpty; - - internal bool Timeout { get; private set; } - - #endregion - - #region Functionality - - internal async ValueTask ReadAsync(bool force = false) - { - if (ReadRequired || force) - { - Cancellation ??= new CancellationTokenSource(); - - try - { - Cancellation.CancelAfter(Configuration.RequestReadTimeout); - - _data = (await Reader.ReadAsync(Cancellation.Token)).Buffer; - - Cancellation.CancelAfter(int.MaxValue); - } - catch (OperationCanceledException) - { - Cancellation.Dispose(); - Cancellation = null; - - Timeout = true; - - return null; - } - } - - return Data.Length; - } - - internal void Advance(long bytes) - { - _data = Data.Slice(bytes); - Reader.AdvanceTo(_data.Value.Start); - } - - #endregion - - #region Disposing - - private bool _disposedValue; - - public void Dispose() - { - if (!_disposedValue) - { - if (Cancellation is not null) - { - Cancellation.Dispose(); - Cancellation = null; - } - - _disposedValue = true; - } - } - - #endregion - -} diff --git a/Engine/Internal/Protocol/RequestQuery.cs b/Engine/Internal/Protocol/RequestQuery.cs deleted file mode 100644 index 51e778c50..000000000 --- a/Engine/Internal/Protocol/RequestQuery.cs +++ /dev/null @@ -1,14 +0,0 @@ -using GenHTTP.Api.Protocol; - -namespace GenHTTP.Engine.Internal.Protocol; - -public sealed class RequestQuery : Dictionary, IRequestQuery -{ - private const int DefaultSize = 12; - - public RequestQuery() : base(DefaultSize, StringComparer.OrdinalIgnoreCase) - { - - } - -} diff --git a/Engine/Internal/Protocol/ResponseHandler.cs b/Engine/Internal/Protocol/ResponseHandler.cs index 521788558..61b6214f3 100644 --- a/Engine/Internal/Protocol/ResponseHandler.cs +++ b/Engine/Internal/Protocol/ResponseHandler.cs @@ -1,120 +1,122 @@ -using System.Net.Sockets; +using System.Buffers; +using System.Runtime.CompilerServices; -using GenHTTP.Api.Infrastructure; using GenHTTP.Api.Protocol; +using GenHTTP.Api.Protocol.Raw; -using GenHTTP.Engine.Internal.Utilities; -using GenHTTP.Engine.Shared.Infrastructure; +using GenHTTP.Engine.Internal.Context; +using GenHTTP.Engine.Internal.Protocol.Sinks; namespace GenHTTP.Engine.Internal.Protocol; -internal sealed class ResponseHandler +internal sealed class ResponseHandler : IResponseSink { + private static readonly ReadOnlyMemory ServerHeaderName = "Server"u8.ToArray(); - #region Get-/Setters + private readonly RegularSink _regularSink; - private IServer Server { get; } + private readonly ChunkedSink _chunkedSink; - private Socket Socket { get; } + // todo: have a separate sink - private PoolBufferedStream Output { get; } + public IBufferWriter Writer => Context.Writer; - private NetworkConfiguration Configuration { get; } + public Stream Stream => Context.Stream; - #endregion + private ClientContext Context { get; } #region Initialization - internal ResponseHandler(IServer server, Socket socket, PoolBufferedStream output, NetworkConfiguration configuration) + internal ResponseHandler(ClientContext context) { - Server = server; - Socket = socket; - - Output = output; + Context = context; - Configuration = configuration; + _regularSink = new(Context); + _chunkedSink = new(Context); } #endregion #region Functionality - internal async ValueTask Handle(IRequest? request, IResponse response, HttpProtocol version, bool keepAlive, bool dataRemaining) + internal async ValueTask HandleAsync(IRequest? request, IResponse response, HttpProtocol version, bool keepAlive) { try { - WriteStatus(request, response); - - WriteHeader(response, version, keepAlive); + var raw = response.Raw; - Output.Write("\r\n"u8); + var writer = Context.Writer; + + writer.Write(StatusLine.Get(raw.Status)); - if (ShouldSendBody(request, response)) - { - await WriteBody(response); - } + WriteHeader(raw, version, keepAlive); - var connected = Socket.Connected; + writer.Write("\r\n"u8); - // flush if the client waits for this response - // otherwise save flushes for improved performance when pipelining - if (!dataRemaining && connected) + if (ShouldSendBody(request, response)) { - await Output.FlushAsync(); + await WriteBodyAsync(raw); } if (request != null) { - Server.Companion?.OnRequestHandled(request, response); + Context.Server.Companion?.OnRequestHandled(request, response); } - return connected; + return Context.Connection.Connected; } - catch (Exception e) + catch (Exception) { - Server.Companion?.OnServerError(ServerErrorScope.ClientConnection, request?.Client.IPAddress, e); + // todo + // Server.Companion?.OnServerError(ServerErrorScope.ClientConnection, request?.Client.IPAddress, e); return false; } } - private static bool ShouldSendBody(IRequest? request, IResponse response) => (request == null || request.Method.KnownMethod != RequestMethod.Head) && + private static bool ShouldSendBody(IRequest? request, IResponse response) => true; // todo + /*(request == null || request.Method.KnownMethod != RequestMethod.Head) && ( response.ContentLength > 0 || response.Content?.Length > 0 || response.ContentType is not null || response.ContentEncoding is not null || response.Connection == Connection.Upgrade - ); + );*/ - private void WriteStatus(IRequest? request, IResponse response) + private void WriteHeader(IRawResponse response, HttpProtocol version, bool keepAlive) { - Output.Write((request?.ProtocolType == HttpProtocol.Http11) ? "HTTP/1.1 "u8 : "HTTP/1.0 "u8); - Output.Write(response.Status.RawStatus); - Output.Write(" "u8); + var context = Context; - Output.Write(response.Status.Phrase); + var writer = context.Writer; - Output.Write("\r\n"u8); - } + if (!response.Headers.ContainsKey(ServerHeaderName)) + { + writer.Write(ServerHeader.GetValue(context).Span); + } - private void WriteHeader(IResponse response, HttpProtocol version, bool keepAlive) - { - if (response.Headers.TryGetValue("Server", out var server)) + writer.Write(DateHeader.GetValue().Span); + + var content = response.Content; + + if (content != null) { - Output.Write("Server: "u8); - Output.Write(server); - Output.Write("\r\n"u8); + var length = content.Length; + + if (length != null) + { + writer.Write("Content-Length: "u8); + writer.Write(length.Value); + writer.Write("\r\n"u8); + } + else + { + writer.Write("Transfer-Encoding: chunked\r\n"u8); + } } else { - Output.Write("Server: GenHTTP/"u8); - Output.Write(Server.Version); - Output.Write("\r\n"u8); + writer.Write("Content-Length: 0\r\n"u8); } - Output.Write("Date: "u8); - Output.Write(DateHeader.GetValue()); - Output.Write("\r\n"u8); - - if (response.Connection == Connection.Upgrade) + /*if (response.Connection == Connection.Upgrade) { Output.Write("Connection: Upgrade\r\n"u8); } @@ -126,9 +128,9 @@ private void WriteHeader(IResponse response, HttpProtocol version, bool keepAliv { // HTTP/1.1 connections are persistent by default so we do not need to send a Keep-Alive header Output.Write("Connection: Close\r\n"u8); - } + }*/ - if (response.ContentType is not null) + /*if (response.ContentType is not null) { Output.Write("Content-Type: "u8); Output.Write(response.ContentType.RawType); @@ -172,70 +174,80 @@ private void WriteHeader(IResponse response, HttpProtocol version, bool keepAliv Output.Write("Expires: "u8); Output.Write(response.Expires.Value); Output.Write("\r\n"u8); - } + }*/ - var serverSpan = "Server".AsSpan(); + var headers = response.Headers; - foreach (var header in response.Headers) + for (var i = 0; i < headers.Count; i++) { - var keySpan = header.Key.AsSpan(); + var header = headers[i]; - if (!keySpan.Equals(serverSpan, StringComparison.OrdinalIgnoreCase)) - { - Output.Write(header.Key); - Output.Write(": "u8); - Output.Write(header.Value); - Output.Write("\r\n"u8); - } + writer.Write(header.Key.Span); + writer.Write(": "u8); + writer.Write(header.Value.Span); + writer.Write("\r\n"u8); } - if (response.HasCookies) + /*if (response.HasCookies) { foreach (var cookie in response.Cookies) { WriteCookie(cookie.Value); } - } + }*/ } - private async ValueTask WriteBody(IResponse response) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ValueTask WriteBodyAsync(IRawResponse response) { - if (response.Content is not null) + var content = response.Content; + + if (content is null) { - if (response.ContentLength is null && (response.Connection != Connection.Upgrade)) - { - await using var chunked = new ChunkedStream(Output); + return ValueTask.CompletedTask; + } - await response.Content.WriteAsync(chunked, Configuration.TransferBufferSize); + var length = content.Length; - chunked.Finish(); - } - else - { - await response.Content.WriteAsync(Output, Configuration.TransferBufferSize); - } + if (length is null) // todo: && (response.Connection != Connection.Upgrade) + { + return WriteChunked(content); } + + _regularSink.Apply(); + + return content.WriteAsync(_regularSink); + + } + + private async ValueTask WriteChunked(IResponseContent content) + { + _chunkedSink.Apply(); + + await content.WriteAsync(_chunkedSink); + + _chunkedSink.Finish(); } #endregion #region Helpers - private void WriteCookie(Cookie cookie) + /*private void WriteCookie(Cookie cookie) { - Output.Write("Set-Cookie: "u8); - Output.Write(cookie.Name); - Output.Write("="u8); - Output.Write(cookie.Value); + Writer.Write("Set-Cookie: "u8); + Writer.Write(cookie.Name); + Writer.Write("="u8); + Writer.Write(cookie.Value); if (cookie.MaxAge is not null) { - Output.Write("; Max-Age="u8); - Output.Write(cookie.MaxAge.Value); + Writer.Write("; Max-Age="u8); + Writer.Write(cookie.MaxAge.Value); } - Output.Write("; Path=/\r\n"u8); - } + Writer.Write("; Path=/\r\n"u8); + }*/ #endregion diff --git a/Engine/Internal/Protocol/ServerHeader.cs b/Engine/Internal/Protocol/ServerHeader.cs new file mode 100644 index 000000000..95b22e1da --- /dev/null +++ b/Engine/Internal/Protocol/ServerHeader.cs @@ -0,0 +1,30 @@ +using System.Runtime.CompilerServices; +using System.Text; +using GenHTTP.Engine.Internal.Context; + +namespace GenHTTP.Engine.Internal.Protocol; + +internal static class ServerHeader +{ + private static ReadOnlyMemory _cached; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ReadOnlyMemory GetValue(ClientContext context) + { + if (!_cached.IsEmpty) + { + return _cached; + } + + var version = context.Server.Version; + var buffer = new byte[16 + version.Length + 2]; + + "Server: GenHTTP/"u8.CopyTo(buffer); + Encoding.ASCII.GetBytes(version, buffer.AsSpan(16)); + "\r\n"u8.CopyTo(buffer.AsSpan(16 + version.Length)); + + _cached = buffer; + return _cached; + } + +} diff --git a/Engine/Internal/Protocol/Sinks/ChunkedSink.cs b/Engine/Internal/Protocol/Sinks/ChunkedSink.cs new file mode 100644 index 000000000..7a9e4a0e4 --- /dev/null +++ b/Engine/Internal/Protocol/Sinks/ChunkedSink.cs @@ -0,0 +1,25 @@ +using System.Buffers; + +using GenHTTP.Api.Protocol; +using GenHTTP.Engine.Internal.Context; + +namespace GenHTTP.Engine.Internal.Protocol.Sinks; + +internal sealed class ChunkedSink(ClientContext context) : IResponseSink +{ + private readonly ChunkedWriter _writer = new(context); + + private ContextStream? _stream; + + public IBufferWriter Writer => _writer; + + public Stream Stream => _stream ??= new ContextStream(_writer); + + public void Apply() + { + _stream = null; + } + + public void Finish() => _writer.Finish(); + +} diff --git a/Engine/Internal/Protocol/Sinks/ChunkedWriter.cs b/Engine/Internal/Protocol/Sinks/ChunkedWriter.cs new file mode 100644 index 000000000..f55ae0b8a --- /dev/null +++ b/Engine/Internal/Protocol/Sinks/ChunkedWriter.cs @@ -0,0 +1,69 @@ +using System.Buffers; + +using GenHTTP.Engine.Internal.Context; + +namespace GenHTTP.Engine.Internal.Protocol.Sinks; + +internal sealed class ChunkedWriter(ClientContext context) : IBufferWriter +{ + private const int HeaderSize = 10; + private const int TrailerSize = 2; + private const int Overhead = HeaderSize + TrailerSize; + + private Memory _activeMemory; + + public Memory GetMemory(int sizeHint = 0) + { + _activeMemory = context.Writer.GetMemory(Math.Max(sizeHint, 1) + Overhead); + + return _activeMemory.Slice(HeaderSize, _activeMemory.Length - Overhead); + } + + public Span GetSpan(int sizeHint = 0) => GetMemory(sizeHint).Span; + + public void Advance(int count) + { + if (count == 0) return; + + var span = _activeMemory.Span; + + WriteHex8((uint)count, span); + + span[HeaderSize + count] = (byte)'\r'; + span[HeaderSize + count + 1] = (byte)'\n'; + + context.Writer.Advance(HeaderSize + count + TrailerSize); + + _activeMemory = default; + } + + public void Finish() + { + var writer = context.Writer; + + var span = writer.GetSpan(5); + + span[0] = (byte)'0'; + span[1] = (byte)'\r'; + span[2] = (byte)'\n'; + span[3] = (byte)'\r'; + span[4] = (byte)'\n'; + + writer.Advance(5); + } + + private static void WriteHex8(uint value, Span dest) + { + const string hex = "0123456789ABCDEF"; + + for (var i = 7; i >= 0; i--) + { + dest[i] = (byte)hex[(int)(value & 0xF)]; + value >>= 4; + } + + dest[8] = (byte)'\r'; + dest[9] = (byte)'\n'; + } + +} diff --git a/Engine/Internal/Protocol/Sinks/ContextStream.cs b/Engine/Internal/Protocol/Sinks/ContextStream.cs new file mode 100644 index 000000000..1a2d0ef8c --- /dev/null +++ b/Engine/Internal/Protocol/Sinks/ContextStream.cs @@ -0,0 +1,41 @@ +using System.Buffers; + +namespace GenHTTP.Engine.Internal.Protocol.Sinks; + +// todo: add reading support via PipeReader + +public sealed class ContextStream(IBufferWriter writer) : Stream +{ + + public override bool CanRead => false; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() { } + + public override void Write(ReadOnlySpan buffer) + { + var dest = writer.GetSpan(buffer.Length); + buffer[..Math.Min(buffer.Length, dest.Length)].CopyTo(dest); + writer.Advance(Math.Min(buffer.Length, dest.Length)); + } + + public override void Write(byte[] buffer, int offset, int count) => Write(buffer.AsSpan(offset, count)); + + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + +} diff --git a/Engine/Internal/Protocol/Sinks/RegularSink.cs b/Engine/Internal/Protocol/Sinks/RegularSink.cs new file mode 100644 index 000000000..926b3ea8b --- /dev/null +++ b/Engine/Internal/Protocol/Sinks/RegularSink.cs @@ -0,0 +1,20 @@ +using System.Buffers; +using GenHTTP.Api.Protocol; +using GenHTTP.Engine.Internal.Context; + +namespace GenHTTP.Engine.Internal.Protocol.Sinks; + +internal sealed class RegularSink(ClientContext context) : IResponseSink +{ + private ContextStream? _stream; + + public IBufferWriter Writer => context.Writer; + + public Stream Stream => _stream ??= new ContextStream(Writer); + + public void Apply() + { + _stream = null; + } + +} diff --git a/Engine/Internal/Protocol/StatusLine.cs b/Engine/Internal/Protocol/StatusLine.cs new file mode 100644 index 000000000..1980330c1 --- /dev/null +++ b/Engine/Internal/Protocol/StatusLine.cs @@ -0,0 +1,89 @@ +using GenHTTP.Api.Protocol; + +namespace GenHTTP.Engine.Internal.Protocol; + +internal static class StatusLine +{ + private static readonly byte[][] Lines = Init(); + + public static byte[] Get(ResponseStatus status) => Lines[(int)status]; + + private static byte[][] Init() + { + var arr = new byte[600][]; + + // 1xx + arr[100] = "HTTP/1.1 100 Continue\r\n"u8.ToArray(); + arr[101] = "HTTP/1.1 101 Switching Protocols\r\n"u8.ToArray(); + arr[102] = "HTTP/1.1 102 Processing\r\n"u8.ToArray(); // WebDAV + + // 2xx + arr[200] = "HTTP/1.1 200 OK\r\n"u8.ToArray(); + arr[201] = "HTTP/1.1 201 Created\r\n"u8.ToArray(); + arr[202] = "HTTP/1.1 202 Accepted\r\n"u8.ToArray(); + arr[203] = "HTTP/1.1 203 Non-Authoritative Information\r\n"u8.ToArray(); + arr[204] = "HTTP/1.1 204 No Content\r\n"u8.ToArray(); + arr[205] = "HTTP/1.1 205 Reset Content\r\n"u8.ToArray(); + arr[206] = "HTTP/1.1 206 Partial Content\r\n"u8.ToArray(); + arr[207] = "HTTP/1.1 207 Multi-Status\r\n"u8.ToArray(); // WebDAV + arr[208] = "HTTP/1.1 208 Already Reported\r\n"u8.ToArray(); // WebDAV + arr[226] = "HTTP/1.1 226 IM Used\r\n"u8.ToArray(); // Delta encoding + + // 3xx + arr[300] = "HTTP/1.1 300 Multiple Choices\r\n"u8.ToArray(); + arr[301] = "HTTP/1.1 301 Moved Permanently\r\n"u8.ToArray(); + arr[302] = "HTTP/1.1 302 Found\r\n"u8.ToArray(); + arr[303] = "HTTP/1.1 303 See Other\r\n"u8.ToArray(); + arr[304] = "HTTP/1.1 304 Not Modified\r\n"u8.ToArray(); + arr[305] = "HTTP/1.1 305 Use Proxy\r\n"u8.ToArray(); + arr[307] = "HTTP/1.1 307 Temporary Redirect\r\n"u8.ToArray(); + arr[308] = "HTTP/1.1 308 Permanent Redirect\r\n"u8.ToArray(); + + // 4xx + arr[400] = "HTTP/1.1 400 Bad Request\r\n"u8.ToArray(); + arr[401] = "HTTP/1.1 401 Unauthorized\r\n"u8.ToArray(); + arr[402] = "HTTP/1.1 402 Payment Required\r\n"u8.ToArray(); + arr[403] = "HTTP/1.1 403 Forbidden\r\n"u8.ToArray(); + arr[404] = "HTTP/1.1 404 Not Found\r\n"u8.ToArray(); + arr[405] = "HTTP/1.1 405 Method Not Allowed\r\n"u8.ToArray(); + arr[406] = "HTTP/1.1 406 Not Acceptable\r\n"u8.ToArray(); + arr[407] = "HTTP/1.1 407 Proxy Authentication Required\r\n"u8.ToArray(); + arr[408] = "HTTP/1.1 408 Request Timeout\r\n"u8.ToArray(); + arr[409] = "HTTP/1.1 409 Conflict\r\n"u8.ToArray(); + arr[410] = "HTTP/1.1 410 Gone\r\n"u8.ToArray(); + arr[411] = "HTTP/1.1 411 Length Required\r\n"u8.ToArray(); + arr[412] = "HTTP/1.1 412 Precondition Failed\r\n"u8.ToArray(); + arr[413] = "HTTP/1.1 413 Payload Too Large\r\n"u8.ToArray(); + arr[414] = "HTTP/1.1 414 URI Too Long\r\n"u8.ToArray(); + arr[415] = "HTTP/1.1 415 Unsupported Media Type\r\n"u8.ToArray(); + arr[416] = "HTTP/1.1 416 Range Not Satisfiable\r\n"u8.ToArray(); + arr[417] = "HTTP/1.1 417 Expectation Failed\r\n"u8.ToArray(); + arr[418] = "HTTP/1.1 418 I'm a Teapot\r\n"u8.ToArray(); + arr[421] = "HTTP/1.1 421 Misdirected Request\r\n"u8.ToArray(); + arr[422] = "HTTP/1.1 422 Unprocessable Entity\r\n"u8.ToArray(); + arr[423] = "HTTP/1.1 423 Locked\r\n"u8.ToArray(); + arr[424] = "HTTP/1.1 424 Failed Dependency\r\n"u8.ToArray(); + arr[425] = "HTTP/1.1 425 Too Early\r\n"u8.ToArray(); + arr[426] = "HTTP/1.1 426 Upgrade Required\r\n"u8.ToArray(); + arr[428] = "HTTP/1.1 428 Precondition Required\r\n"u8.ToArray(); + arr[429] = "HTTP/1.1 429 Too Many Requests\r\n"u8.ToArray(); + arr[431] = "HTTP/1.1 431 Request Header Fields Too Large\r\n"u8.ToArray(); + arr[451] = "HTTP/1.1 451 Unavailable For Legal Reasons\r\n"u8.ToArray(); + + // 5xx + arr[500] = "HTTP/1.1 500 Internal Server Error\r\n"u8.ToArray(); + arr[501] = "HTTP/1.1 501 Not Implemented\r\n"u8.ToArray(); + arr[502] = "HTTP/1.1 502 Bad Gateway\r\n"u8.ToArray(); + arr[503] = "HTTP/1.1 503 Service Unavailable\r\n"u8.ToArray(); + arr[504] = "HTTP/1.1 504 Gateway Timeout\r\n"u8.ToArray(); + arr[505] = "HTTP/1.1 505 HTTP Version Not Supported\r\n"u8.ToArray(); + arr[506] = "HTTP/1.1 506 Variant Also Negotiates\r\n"u8.ToArray(); + arr[507] = "HTTP/1.1 507 Insufficient Storage\r\n"u8.ToArray(); + arr[508] = "HTTP/1.1 508 Loop Detected\r\n"u8.ToArray(); + arr[510] = "HTTP/1.1 510 Not Extended\r\n"u8.ToArray(); + arr[511] = "HTTP/1.1 511 Network Authentication Required\r\n"u8.ToArray(); + + return arr; + } + +} diff --git a/Engine/Internal/Protocol/StreamExtensions.cs b/Engine/Internal/Protocol/StreamExtensions.cs deleted file mode 100644 index 0ea96d78f..000000000 --- a/Engine/Internal/Protocol/StreamExtensions.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Runtime.CompilerServices; - -using GenHTTP.Engine.Internal.Utilities; - -namespace GenHTTP.Engine.Internal.Protocol; - -internal static class StreamExtensions -{ - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void Write(this PoolBufferedStream stream, string value) - { - Span buffer = stackalloc byte[value.Length]; - - for (var i = 0; i < value.Length; i++) - { - buffer[i] = (byte)value[i]; - } - - stream.Write(buffer); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void Write(this PoolBufferedStream stream, long number) - { - Span buffer = stackalloc byte[20]; - - if (number.TryFormat(buffer, out var written)) - { - stream.Write(buffer[..written]); - } - else - { - throw new InvalidOperationException("Unable to write number to stream"); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void Write(this PoolBufferedStream stream, ulong number) - { - Span buffer = stackalloc byte[20]; - - if (number.TryFormat(buffer, out var written)) - { - stream.Write(buffer[..written]); - } - else - { - throw new InvalidOperationException("Unable to write number to stream"); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void Write(this PoolBufferedStream stream, DateTime time) - { - Span charBuffer = stackalloc char[29]; // RFC1123 format is 29 chars - - if (time.ToUniversalTime().TryFormat(charBuffer, out var written, "r")) - { - Span byteBuffer = stackalloc byte[written]; - - for (var i = 0; i < written; i++) - { - byteBuffer[i] = (byte)charBuffer[i]; - } - - stream.Write(byteBuffer); - } - else - { - throw new InvalidOperationException("Unable to write date time to stream"); - } - } - -} diff --git a/Engine/Internal/Protocol/WriterExtensions.cs b/Engine/Internal/Protocol/WriterExtensions.cs new file mode 100644 index 000000000..325bc94f6 --- /dev/null +++ b/Engine/Internal/Protocol/WriterExtensions.cs @@ -0,0 +1,67 @@ +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace GenHTTP.Engine.Internal.Protocol; + +internal static class BufferWriterExtensions +{ + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Write(this IBufferWriter writer, string value) + { + Span span = writer.GetSpan(value.Length); + + for (var i = 0; i < value.Length; i++) + { + span[i] = (byte)value[i]; + } + + writer.Advance(value.Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Write(this IBufferWriter writer, long number) + { + Span buffer = writer.GetSpan(20); + + if (number.TryFormat(buffer, out var written)) + { + writer.Advance(written); + } + else + { + throw new InvalidOperationException("Unable to write number to buffer"); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Write(this IBufferWriter writer, ulong number) + { + Span buffer = writer.GetSpan(20); + + if (number.TryFormat(buffer, out var written)) + { + writer.Advance(written); + } + else + { + throw new InvalidOperationException("Unable to write number to buffer"); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Write(this IBufferWriter writer, DateTime time) + { + Span charBuffer = writer.GetSpan(29); // RFC1123 format is 29 chars + + if (time.ToUniversalTime().TryFormat(charBuffer, out var written, "r")) + { + writer.Advance(written); + } + else + { + throw new InvalidOperationException("Unable to write date time to buffer"); + } + } + +} diff --git a/Engine/Shared/GenHTTP.Engine.Shared.csproj b/Engine/Shared/GenHTTP.Engine.Shared.csproj index 2a025ecf8..c0592676f 100644 --- a/Engine/Shared/GenHTTP.Engine.Shared.csproj +++ b/Engine/Shared/GenHTTP.Engine.Shared.csproj @@ -14,6 +14,10 @@ + + + + diff --git a/Engine/Shared/Infrastructure/ConsoleCompanion.cs b/Engine/Shared/Infrastructure/ConsoleCompanion.cs index 8d17ecb3e..2bbbbb4b3 100644 --- a/Engine/Shared/Infrastructure/ConsoleCompanion.cs +++ b/Engine/Shared/Infrastructure/ConsoleCompanion.cs @@ -10,11 +10,13 @@ public sealed class ConsoleCompanion : IServerCompanion public void OnRequestHandled(IRequest request, IResponse response) { - Console.WriteLine($"REQ - {request.Client.IPAddress} - {request.Method.RawMethod} {request.Target.Path} - {response.Status.RawStatus} - {response.ContentLength ?? 0}"); + // todo + // Console.WriteLine($"REQ - {request.Client.IPAddress} - {request.Method.RawMethod} {request.Target.Path} - {response.Status.RawStatus} - {response.ContentLength ?? 0}"); } public void OnServerError(ServerErrorScope scope, IPAddress? client, Exception error) { Console.WriteLine($"ERR - {client: 'n/a'} - {scope} - {error}"); } + } diff --git a/Engine/Shared/Types/Extensions.cs b/Engine/Shared/Types/Extensions.cs new file mode 100644 index 000000000..3a0ca9b37 --- /dev/null +++ b/Engine/Shared/Types/Extensions.cs @@ -0,0 +1,13 @@ +using System.Text; + +namespace GenHTTP.Engine.Shared.Types; + +public static class Extensions +{ + private static readonly Encoding AsciiEncoding = Encoding.ASCII; + + public static string GetString(this ReadOnlyMemory memory) => AsciiEncoding.GetString(memory.Span); + + public static ReadOnlyMemory GetMemory(this string str) => AsciiEncoding.GetBytes(str); + +} diff --git a/Engine/Shared/Types/KeyValueList.cs b/Engine/Shared/Types/KeyValueList.cs new file mode 100644 index 000000000..52f88f6e7 --- /dev/null +++ b/Engine/Shared/Types/KeyValueList.cs @@ -0,0 +1,13 @@ +using GenHTTP.Api.Protocol; +using GenHTTP.Api.Protocol.Raw; + +namespace GenHTTP.Engine.Shared.Types; + +public sealed class KeyValueList(IRawKeyValueList source) : IKeyValueList +{ + + public string? GetValue(string key) => source.GetEntry(key.GetMemory())?.GetString(); + + public string? GetValue(ReadOnlyMemory key) => GetValue(key.GetString()); + +} diff --git a/Engine/Shared/Types/Raw/EditableKeyValueList.cs b/Engine/Shared/Types/Raw/EditableKeyValueList.cs new file mode 100644 index 000000000..b876060bc --- /dev/null +++ b/Engine/Shared/Types/Raw/EditableKeyValueList.cs @@ -0,0 +1,20 @@ +using GenHTTP.Api.Protocol.Raw; + +namespace GenHTTP.Engine.Shared.Types.Raw; + +public class EditableKeyValueList : IRawKeyValueList +{ + private readonly List, ReadOnlyMemory>> _store = new(); + + public int Count => _store.Count; + + public KeyValuePair, ReadOnlyMemory> this[int index] => _store[index]; + + public void Add(ReadOnlyMemory key, ReadOnlyMemory value) + { + _store.Add(new KeyValuePair, ReadOnlyMemory>(key, value)); + } + + public void Clear() => _store.Clear(); + +} diff --git a/Engine/Shared/Types/Raw/RawKeyValueList.cs b/Engine/Shared/Types/Raw/RawKeyValueList.cs new file mode 100644 index 000000000..32f82e932 --- /dev/null +++ b/Engine/Shared/Types/Raw/RawKeyValueList.cs @@ -0,0 +1,13 @@ +using GenHTTP.Api.Protocol.Raw; +using Glyph = Glyph11.Protocol; + +namespace GenHTTP.Engine.Shared.Types.Raw; + +public sealed class RawKeyValueList(Glyph.KeyValueList source) : IRawKeyValueList +{ + + public int Count => source.Count; + + public KeyValuePair, ReadOnlyMemory> this[int index] => source[index]; + +} diff --git a/Engine/Shared/Types/Raw/RawRequest.cs b/Engine/Shared/Types/Raw/RawRequest.cs new file mode 100644 index 000000000..e2387f574 --- /dev/null +++ b/Engine/Shared/Types/Raw/RawRequest.cs @@ -0,0 +1,67 @@ +using GenHTTP.Api.Protocol.Raw; +using Glyph11.Protocol; + +namespace GenHTTP.Engine.Shared.Types.Raw; + +public sealed class RawRequest : IRawRequest +{ + private bool _bodyObtained; + + private readonly RawRequestHeader _header; + + private IRawRequestHeader? _retainedHeader; + + public IRawRequestHeader Header + { + get + { + if (!_bodyObtained) + { + return _header; + } + + if (_retainedHeader == null) + { + throw new InvalidOperationException("Header information can no longer be accessed"); + } + + return _retainedHeader; + } + } + + internal BinaryRequest Source { get; } + + public RawRequest() + { + Source = new(); + + _header = new(this); + } + + public IRawRequestBody? GetBody(HeaderAccess headerAccess) + { + if (_bodyObtained) + { + throw new InvalidOperationException("Request body can only be fetched once."); + } + + if (headerAccess == HeaderAccess.Retain) + { + _retainedHeader = new RetainedRequestHeader(_header); + } + + _bodyObtained = true; + + // todo + return null; + } + + public void Apply() + { + _header.Apply(); + + _bodyObtained = false; + _retainedHeader = null; + } + +} diff --git a/Engine/Shared/Types/Raw/RawRequestHeader.cs b/Engine/Shared/Types/Raw/RawRequestHeader.cs new file mode 100644 index 000000000..f63214a84 --- /dev/null +++ b/Engine/Shared/Types/Raw/RawRequestHeader.cs @@ -0,0 +1,40 @@ +using GenHTTP.Api.Protocol.Raw; + +namespace GenHTTP.Engine.Shared.Types.Raw; + +public class RawRequestHeader : IRawRequestHeader +{ + private readonly RawRequest _request; + + private readonly RawKeyValueList _headers, _query; + + private readonly RawRequestTarget _target; + + public ReadOnlyMemory Method => _request.Source.Method; + + public ReadOnlyMemory Path => _request.Source.Path; + + public IRawRequestTarget Target => _target; + + public ReadOnlyMemory Version => _request.Source.Version; + + public IRawKeyValueList Headers => _headers; + + public IRawKeyValueList Query => _query; + + public RawRequestHeader(RawRequest request) + { + _request = request; + + _headers = new RawKeyValueList(request.Source.Headers); + _query = new RawKeyValueList(request.Source.QueryParameters); + + _target = new RawRequestTarget(); + } + + public void Apply() + { + _target.Apply(Path); + } + +} diff --git a/Engine/Shared/Types/Raw/RawRequestTarget.cs b/Engine/Shared/Types/Raw/RawRequestTarget.cs new file mode 100644 index 000000000..1f034e0aa --- /dev/null +++ b/Engine/Shared/Types/Raw/RawRequestTarget.cs @@ -0,0 +1,71 @@ +using System.Runtime.CompilerServices; +using GenHTTP.Api.Protocol.Raw; + +namespace GenHTTP.Engine.Shared.Types.Raw; + +public sealed class RawRequestTarget : IRawRequestTarget +{ + private ReadOnlyMemory _path = ReadOnlyMemory.Empty; + + private int _offset; + + public ReadOnlyMemory? Current { get; private set; } + + public void Apply(ReadOnlyMemory path) + { + _path = path; + + _offset = 0; + Current = null; + + MoveNext(); + } + + public void Advance(int segments = 1) + { + while (segments-- > 0 && Current != null) + { + MoveNext(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void MoveNext() + { + var span = _path.Span; + var length = span.Length; + + if (length < 2) + { + Current = null; + return; + } + + while (_offset < length && span[_offset] == (byte)'/') + { + _offset++; + } + + if (_offset >= length) + { + Current = null; + return; + } + + var start = _offset; + + var idx = span[_offset..].IndexOf((byte)'/'); + + if (idx < 0) + { + _offset = length; + Current = _path.Slice(start, length - start); + } + else + { + _offset += idx; + Current = _path.Slice(start, idx); + } + } + +} diff --git a/Engine/Shared/Types/Raw/RawResponse.cs b/Engine/Shared/Types/Raw/RawResponse.cs new file mode 100644 index 000000000..7abcf9e27 --- /dev/null +++ b/Engine/Shared/Types/Raw/RawResponse.cs @@ -0,0 +1,27 @@ +using GenHTTP.Api.Protocol; +using GenHTTP.Api.Protocol.Raw; + +namespace GenHTTP.Engine.Shared.Types.Raw; + +public class RawResponse : IRawResponse +{ + private readonly EditableKeyValueList _headers = new(); + + public ResponseStatus Status { get; set; } + + public EditableKeyValueList EditableHeaders => _headers; + + public IRawKeyValueList Headers => _headers; + + public IResponseContent? Content { get; set; } + + public void Reset() + { + Status = ResponseStatus.NoContent; + + Content = null; + + _headers.Clear(); + } + +} diff --git a/Engine/Shared/Types/Raw/RawResponseBuilder.cs b/Engine/Shared/Types/Raw/RawResponseBuilder.cs new file mode 100644 index 000000000..bd5ed9186 --- /dev/null +++ b/Engine/Shared/Types/Raw/RawResponseBuilder.cs @@ -0,0 +1,32 @@ +using GenHTTP.Api.Protocol; +using GenHTTP.Api.Protocol.Raw; + +namespace GenHTTP.Engine.Shared.Types.Raw; + +public class RawResponseBuilder(Response response, ResponseBuilder builder) : IRawResponseBuilder +{ + + public IRawResponseBuilder Status(ResponseStatus code) + { + response.Source.Status = code; + return this; + } + + + public IRawResponseBuilder Header(ReadOnlyMemory name, ReadOnlyMemory value) + { + response.Source.EditableHeaders.Add(name, value); + return this; + } + + public IRawResponseBuilder Content(IResponseContent? content) + { + response.Source.Content = content; + return this; + } + + public IResponseBuilder Unraw() => builder; + + public IResponse Build() => response; + +} diff --git a/Engine/Shared/Types/Raw/RetainedKeyValueList.cs b/Engine/Shared/Types/Raw/RetainedKeyValueList.cs new file mode 100644 index 000000000..b6c4daca7 --- /dev/null +++ b/Engine/Shared/Types/Raw/RetainedKeyValueList.cs @@ -0,0 +1,25 @@ +using GenHTTP.Api.Protocol.Raw; + +namespace GenHTTP.Engine.Shared.Types.Raw; + +public class RetainedKeyValueList : IRawKeyValueList +{ + private readonly List, ReadOnlyMemory>> _items; + + public int Count => _items.Count; + + public RetainedKeyValueList(IRawKeyValueList source) + { + _items = new(source.Count); + + for (int i = 0; i < source.Count; i++) + { + var pair = source[i]; + + _items.Add(new (pair.Key.ToArray(), pair.Value.ToArray())); + } + } + + public KeyValuePair, ReadOnlyMemory> this[int index] => _items[index]; + +} diff --git a/Engine/Shared/Types/Raw/RetainedRequestHeader.cs b/Engine/Shared/Types/Raw/RetainedRequestHeader.cs new file mode 100644 index 000000000..f65898986 --- /dev/null +++ b/Engine/Shared/Types/Raw/RetainedRequestHeader.cs @@ -0,0 +1,36 @@ +using GenHTTP.Api.Protocol.Raw; + +namespace GenHTTP.Engine.Shared.Types.Raw; + +public class RetainedRequestHeader : IRawRequestHeader +{ + + public ReadOnlyMemory Method { get; } + + public ReadOnlyMemory Path { get; } + + public IRawRequestTarget Target { get; } + + public IRawKeyValueList Query { get; } + + public ReadOnlyMemory Version { get; } + + public IRawKeyValueList Headers { get; } + + internal RetainedRequestHeader(RawRequestHeader source) + { + Method = source.Method.ToArray(); + Path = source.Path.ToArray(); + Version = source.Version.ToArray(); + + var target = new RawRequestTarget(); + + target.Apply(Path); + + Target = target; + + Query = new RetainedKeyValueList(source.Query); + Headers = new RetainedKeyValueList(source.Headers); + } + +} diff --git a/Engine/Shared/Types/Request.cs b/Engine/Shared/Types/Request.cs new file mode 100644 index 000000000..99b45a7a4 --- /dev/null +++ b/Engine/Shared/Types/Request.cs @@ -0,0 +1,100 @@ +using System.Text; + +using GenHTTP.Api.Protocol; +using GenHTTP.Api.Protocol.Raw; +using GenHTTP.Engine.Shared.Types.Raw; +using GenHTTP.Engine.Shared.Utilities; + +using Glyph11.Protocol; + +namespace GenHTTP.Engine.Shared.Types; + +public sealed class Request : IRequest +{ + private static readonly ReadOnlyMemory HostHeader = "Host"u8.ToArray(); + + private readonly RawRequest _raw = new(); + + private readonly ResponseBuilder _response = new(); + + private readonly IKeyValueList _header; + + private readonly IKeyValueList _query; + + private bool _resetRequired = true; + + private RequestMethod? _method; + + public IRawRequest Raw => _raw; + + public IKeyValueList Headers => _header; + + public IKeyValueList Query => _query; + + public IRequestBody? Body { get; } + + public BinaryRequest Source => _raw.Source; + + public RequestMethod Method + { + get + { + if (_method == null) + { + var m = _raw.Header.Method.Span; + + _method = m.Length switch + { + 3 when AsciiComparer.EqualsIgnoreCase(m, "GET"u8) => RequestMethod.Get, + 4 when AsciiComparer.EqualsIgnoreCase(m, "POST"u8) => RequestMethod.Post, + 3 when AsciiComparer.EqualsIgnoreCase(m, "PUT"u8) => RequestMethod.Put, + 6 when AsciiComparer.EqualsIgnoreCase(m, "DELETE"u8) => RequestMethod.Delete, + 4 when AsciiComparer.EqualsIgnoreCase(m, "HEAD"u8) => RequestMethod.Head, + 7 when AsciiComparer.EqualsIgnoreCase(m, "OPTIONS"u8) => RequestMethod.Options, + 5 when AsciiComparer.EqualsIgnoreCase(m, "PATCH"u8) => RequestMethod.Patch, + 5 when AsciiComparer.EqualsIgnoreCase(m, "TRACE"u8) => RequestMethod.Trace, + 7 when AsciiComparer.EqualsIgnoreCase(m, "CONNECT"u8) => RequestMethod.Connect, + _ => RequestMethod.Other + }; + } + + return _method.Value; + } + } + + public string Host => _header.GetValue(HostHeader) ?? throw new InvalidOperationException("Request is missing mandatory host header"); + + public Request() + { + _header = new KeyValueList(_raw.Header.Headers); + _query = new KeyValueList(_raw.Header.Query); + } + + public void Apply() + { + _raw.Apply(); + } + + public void Reset() + { + _raw.Source.Clear(); + _response.Reset(); + + _resetRequired = true; + } + + public IResponseBuilder Respond() + { + if (!_resetRequired) + { + _response.Reset(); + } + else + { + _resetRequired = false; + } + + return _response; + } + +} diff --git a/Engine/Shared/Types/Response.cs b/Engine/Shared/Types/Response.cs index 5a101e962..1a01b5bf3 100644 --- a/Engine/Shared/Types/Response.cs +++ b/Engine/Shared/Types/Response.cs @@ -1,125 +1,19 @@ -using GenHTTP.Api.Protocol; - -using Cookie = GenHTTP.Api.Protocol.Cookie; +using GenHTTP.Api.Protocol; +using GenHTTP.Api.Protocol.Raw; +using GenHTTP.Engine.Shared.Types.Raw; namespace GenHTTP.Engine.Shared.Types; -public sealed class Response : IResponse +public class Response(IResponseBuilder builder) : IResponse { - private static readonly FlexibleResponseStatus StatusOk = new(ResponseStatus.Ok); - - private readonly ResponseHeaderCollection _headers = new(); - - private readonly CookieCollection _cookies = new(); - - #region Initialization - - public Response() - { - Status = StatusOk; - Connection = Api.Protocol.Connection.KeepAlive; - } - - #endregion - - #region Get-/Setters - - public FlexibleResponseStatus Status { get; set; } - - public Connection Connection { get; set; } - - public DateTime? Expires { get; set; } - - public DateTime? Modified { get; set; } - - public FlexibleContentType? ContentType { get; set; } - - public string? ContentEncoding { get; set; } - - public ulong? ContentLength { get; set; } - - public IResponseContent? Content { get; set; } - - public ICookieCollection Cookies => WriteableCookies; - - public bool HasCookies => _cookies.Count > 0; - - public IEditableHeaderCollection Headers => _headers; - - public string? this[string field] - { - get => _headers.GetValueOrDefault(field); - set - { - if (value is not null) - { - _headers[field] = value; - } - else - { - _headers.Remove(field); - } - } - } - - internal CookieCollection WriteableCookies => _cookies; - - #endregion - - #region Functionality - - public void SetCookie(Cookie cookie) - { - WriteableCookies[cookie.Name] = cookie; - } - - public void Reset() - { - Status = StatusOk; - Connection = Connection.KeepAlive; - - _headers.Clear(); - _cookies.Clear(); - - Expires = null; - Modified = null; - - ReleaseContent(); - - ContentType = null; - ContentLength = null; - ContentEncoding = null; - } - - #endregion - - #region IDisposable Support - - private bool _disposed; - - public void Dispose() - { - if (!_disposed) - { - ReleaseContent(); + private readonly RawResponse _raw = new(); - _disposed = true; - } - } + public IRawResponse Raw => _raw; - private void ReleaseContent() - { - if (Content != null) - { - if (Content is IDisposable disposableContent) - { - disposableContent.Dispose(); - } + public IResponseBuilder Rebuild() => builder; - Content = null; - } - } + public RawResponse Source => _raw; - #endregion + public void Reset() => _raw.Reset(); } diff --git a/Engine/Shared/Types/ResponseBuilder.cs b/Engine/Shared/Types/ResponseBuilder.cs index 08a0ea5ef..ab287073d 100644 --- a/Engine/Shared/Types/ResponseBuilder.cs +++ b/Engine/Shared/Types/ResponseBuilder.cs @@ -1,87 +1,37 @@ using GenHTTP.Api.Protocol; +using GenHTTP.Api.Protocol.Raw; +using GenHTTP.Engine.Shared.Types.Raw; namespace GenHTTP.Engine.Shared.Types; -public sealed class ResponseBuilder(Response response) : IResponseBuilder +public class ResponseBuilder : IResponseBuilder { + private readonly Response _response; - #region Functionality + private readonly RawResponseBuilder _raw; - public IResponseBuilder Length(ulong length) + public ResponseBuilder() { - response.ContentLength = length; - return this; - } - - public IResponseBuilder Content(IResponseContent content) - { - response.Content = content; - response.ContentLength = content.Length; - - return this; - } - - public IResponseBuilder Type(FlexibleContentType contentType) - { - response.ContentType = contentType; - return this; - } - - public IResponseBuilder Cookie(Cookie cookie) - { - response.WriteableCookies[cookie.Name] = cookie; - return this; - } - - public IResponseBuilder Header(string key, string value) - { - response.Headers.Add(key, value); - return this; - } - - public IResponseBuilder Encoding(string encoding) - { - response.ContentEncoding = encoding; - return this; - } - - public IResponseBuilder Expires(DateTime expiryDate) - { - response.Expires = expiryDate; - return this; - } - - public IResponseBuilder Modified(DateTime modificationDate) - { - response.Modified = modificationDate; - return this; + _response = new(this); + _raw = new(_response, this); } public IResponseBuilder Status(ResponseStatus status) { - response.Status = new FlexibleResponseStatus(status); + _raw.Status(status); return this; } - public IResponseBuilder Status(int status, string reason) + public IResponseBuilder Header(string name, string value) { - response.Status = new FlexibleResponseStatus(status, reason); + _raw.Header(name.GetMemory(), value.GetMemory()); return this; } - public IResponseBuilder Connection(Connection handling) - { - response.Connection = handling; - return this; - } - - public void Reset() - { - response.Reset(); - } + public IRawResponseBuilder Raw() => _raw; - public IResponse Build() => response; + public IResponse Build() => _response; - #endregion + public void Reset() => _response.Reset(); } diff --git a/Engine/Shared/Utilities/AsciiComparer.cs b/Engine/Shared/Utilities/AsciiComparer.cs new file mode 100644 index 000000000..a3f1129e5 --- /dev/null +++ b/Engine/Shared/Utilities/AsciiComparer.cs @@ -0,0 +1,36 @@ +using System.Runtime.CompilerServices; + +namespace GenHTTP.Engine.Shared.Utilities; + +public static class AsciiComparer +{ + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool EqualsIgnoreCase(ReadOnlyMemory a, ReadOnlyMemory b) + => EqualsIgnoreCase(a.Span, b.Span); + + public static bool EqualsIgnoreCase(ReadOnlySpan a, ReadOnlySpan b) + { + if (a.Length != b.Length) + { + return false; + } + + for (var i = 0; i < a.Length; i++) + { + var x = a[i]; + var y = b[i]; + + if ((uint)(x - 'A') <= 25) x = (byte)(x + 32); + if ((uint)(y - 'A') <= 25) y = (byte)(y + 32); + + if (x != y) + { + return false; + } + } + + return true; + } + +} diff --git a/GenHTTP.slnx b/GenHTTP.slnx index 36643172f..bf0edcb00 100644 --- a/GenHTTP.slnx +++ b/GenHTTP.slnx @@ -1,37 +1,37 @@  - + - + - + - + - - - + + - - + - + - + - + @@ -62,8 +62,8 @@ - + \ No newline at end of file diff --git a/Modules/Caching/GenHTTP.Modules.Caching.csproj b/Modules/Caching/GenHTTP.Modules.Caching.csproj index b499b6407..5943171ac 100644 --- a/Modules/Caching/GenHTTP.Modules.Caching.csproj +++ b/Modules/Caching/GenHTTP.Modules.Caching.csproj @@ -14,7 +14,7 @@ - + diff --git a/Modules/ErrorHandling/ErrorHandler.cs b/Modules/ErrorHandling/ErrorHandler.cs index cd9ca364c..269a52179 100644 --- a/Modules/ErrorHandling/ErrorHandler.cs +++ b/Modules/ErrorHandling/ErrorHandler.cs @@ -1,5 +1,4 @@ -using GenHTTP.Modules.Conversion.Serializers; -using GenHTTP.Modules.ErrorHandling.Mappers; +using GenHTTP.Modules.ErrorHandling.Mappers; using GenHTTP.Modules.ErrorHandling.Provider; namespace GenHTTP.Modules.ErrorHandling; @@ -16,7 +15,7 @@ public static class ErrorHandler /// structured responses. /// /// The default error handler - public static ErrorSentryBuilder Default() => Structured(); + public static ErrorSentryBuilder Default() => From(new TextErrorMapper()); // todo: back to structured /// /// Ans error handler which will render exceptions into @@ -25,14 +24,14 @@ public static class ErrorHandler /// /// The serialization configuration to be used /// A structured error handler - public static ErrorSentryBuilder Structured(SerializationBuilder? serialization = null) => From(new StructuredErrorMapper(serialization?.Build())); + // public static ErrorSentryBuilder Structured(SerializationBuilder? serialization = null) => From(new StructuredErrorMapper(serialization?.Build())); /// /// An error handler which will render exceptions into /// HTML using the current template and IErrorRenderer. /// /// An HTML error handler - public static ErrorSentryBuilder Html() => From(new HtmlErrorMapper()); + // public static ErrorSentryBuilder Html() => From(new HtmlErrorMapper()); /// /// Creates an error handling concern which will use @@ -43,4 +42,5 @@ public static class ErrorHandler /// The mapper to use for exception mapping /// The newly generated concern public static ErrorSentryBuilder From(IErrorMapper mapper) where T : Exception => new(mapper); + } diff --git a/Modules/ErrorHandling/GenHTTP.Modules.ErrorHandling.csproj b/Modules/ErrorHandling/GenHTTP.Modules.ErrorHandling.csproj index bf64ae40d..b0a067c25 100644 --- a/Modules/ErrorHandling/GenHTTP.Modules.ErrorHandling.csproj +++ b/Modules/ErrorHandling/GenHTTP.Modules.ErrorHandling.csproj @@ -12,7 +12,7 @@ - + @@ -21,5 +21,5 @@ - + diff --git a/Modules/ErrorHandling/Mappers/HtmlErrorMapper.cs b/Modules/ErrorHandling/Mappers/HtmlErrorMapper.cs index c9f637781..a7cf35bc7 100644 --- a/Modules/ErrorHandling/Mappers/HtmlErrorMapper.cs +++ b/Modules/ErrorHandling/Mappers/HtmlErrorMapper.cs @@ -1,10 +1,8 @@ -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; -using GenHTTP.Modules.IO; -using GenHTTP.Modules.Pages; +namespace GenHTTP.Modules.ErrorHandling.Mappers; -namespace GenHTTP.Modules.ErrorHandling.Mappers; +// todo +/* public class HtmlErrorMapper : IErrorMapper { @@ -42,3 +40,4 @@ public class HtmlErrorMapper : IErrorMapper .Build(); } } +*/ diff --git a/Modules/ErrorHandling/Mappers/StructuredErrorMapper.cs b/Modules/ErrorHandling/Mappers/StructuredErrorMapper.cs index de6166d3f..59d003a88 100644 --- a/Modules/ErrorHandling/Mappers/StructuredErrorMapper.cs +++ b/Modules/ErrorHandling/Mappers/StructuredErrorMapper.cs @@ -1,15 +1,8 @@ -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; +namespace GenHTTP.Modules.ErrorHandling.Mappers; -using GenHTTP.Modules.Conversion; -using GenHTTP.Modules.Conversion.Serializers; -using GenHTTP.Modules.Conversion.Serializers.Json; -using GenHTTP.Modules.ErrorHandling.Provider; -using GenHTTP.Modules.IO; +// todo -namespace GenHTTP.Modules.ErrorHandling.Mappers; - -public sealed class StructuredErrorMapper : IErrorMapper +/*public sealed class StructuredErrorMapper : IErrorMapper { #region Initialization @@ -88,4 +81,4 @@ private async ValueTask RenderAsync(IRequest request, ErrorMod #endregion -} +}*/ diff --git a/Modules/ErrorHandling/Mappers/TextErrorMapper.cs b/Modules/ErrorHandling/Mappers/TextErrorMapper.cs new file mode 100644 index 000000000..131747381 --- /dev/null +++ b/Modules/ErrorHandling/Mappers/TextErrorMapper.cs @@ -0,0 +1,35 @@ +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using StringContent = GenHTTP.Modules.IO.Strings.StringContent; + +namespace GenHTTP.Modules.ErrorHandling.Mappers; + +public class TextErrorMapper : IErrorMapper +{ + + public ValueTask Map(IRequest request, IHandler handler, Exception error) + { + var response = GetStringResponse(request, error.ToString(), ResponseStatus.InternalServerError); + + return new(response); + } + + public ValueTask GetNotFound(IRequest request, IHandler handler) + { + var response = GetStringResponse(request, "Not Found", ResponseStatus.BadRequest); + + return new(response); + } + + private IResponse GetStringResponse(IRequest request, string text, ResponseStatus status) + { + return request.Respond() + .Raw() + .Status(status) + .Content(new StringContent(text)) + .Unraw() + .Build(); + } + +} diff --git a/Modules/ErrorHandling/Provider/ErrorModel.Serialization.cs b/Modules/ErrorHandling/Provider/ErrorModel.Serialization.cs index f032d1cd9..35f8cc54b 100644 --- a/Modules/ErrorHandling/Provider/ErrorModel.Serialization.cs +++ b/Modules/ErrorHandling/Provider/ErrorModel.Serialization.cs @@ -1,10 +1,7 @@ -using System.Text.Json.Serialization; -using GenHTTP.Modules.ErrorHandling.Mappers; +namespace GenHTTP.Modules.ErrorHandling.Provider; -namespace GenHTTP.Modules.ErrorHandling.Provider; - -[JsonSerializable(typeof(StructuredErrorMapper.ErrorModel))] +/*[JsonSerializable(typeof(StructuredErrorMapper.ErrorModel))] public partial class ErrorHandlingContext : JsonSerializerContext { -} +}*/ diff --git a/Modules/IO/Download.cs b/Modules/IO/Download.cs index 70ed4b2b2..e19fa4891 100644 --- a/Modules/IO/Download.cs +++ b/Modules/IO/Download.cs @@ -1,9 +1,6 @@ -using GenHTTP.Api.Content.IO; -using GenHTTP.Api.Infrastructure; -using GenHTTP.Modules.IO.Providers; - -namespace GenHTTP.Modules.IO; +namespace GenHTTP.Modules.IO; +/* /// /// Generates a file download response for a given resource. /// @@ -22,3 +19,4 @@ public static class Download /// The resource to be provided public static DownloadProviderBuilder From(IResource resource) => new DownloadProviderBuilder().Resource(resource); } +*/ diff --git a/Modules/IO/Embedded/EmbeddedResource.cs b/Modules/IO/Embedded/EmbeddedResource.cs index b2043bfeb..e0131d3a8 100644 --- a/Modules/IO/Embedded/EmbeddedResource.cs +++ b/Modules/IO/Embedded/EmbeddedResource.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Buffers; +using System.Reflection; using GenHTTP.Api.Content.IO; using GenHTTP.Api.Protocol; using GenHTTP.Modules.IO.Streaming; @@ -50,6 +51,11 @@ public EmbeddedResource(Assembly source, string path, string? name, FlexibleCont public ValueTask GetContentAsync() => new(TryGetStream()); + public void Write(IBufferWriter writer) + { + throw new NotImplementedException(); + } + public async ValueTask CalculateChecksumAsync() { if (_checksum is null) diff --git a/Modules/IO/Extensions.RangeSupport.cs b/Modules/IO/Extensions.RangeSupport.cs index a42e076d2..7e985423e 100644 --- a/Modules/IO/Extensions.RangeSupport.cs +++ b/Modules/IO/Extensions.RangeSupport.cs @@ -1,7 +1,6 @@ -using GenHTTP.Api.Infrastructure; - -namespace GenHTTP.Modules.IO; +namespace GenHTTP.Modules.IO; +/* public static class RangeSupportExtensions { @@ -17,3 +16,4 @@ public static IServerHost RangeSupport(this IServerHost host) } } +*/ diff --git a/Modules/IO/Extensions.Request.cs b/Modules/IO/Extensions.Request.cs index a161d5993..7d53b501f 100644 --- a/Modules/IO/Extensions.Request.cs +++ b/Modules/IO/Extensions.Request.cs @@ -18,7 +18,9 @@ public static bool HasType(this IRequest request, params RequestMethod[] methods return false; } - public static string? HostWithoutPort(this IRequest request) + // todo + + /*public static string? HostWithoutPort(this IRequest request) { var host = request.Host; @@ -31,5 +33,6 @@ public static bool HasType(this IRequest request, params RequestMethod[] methods return null; } + */ } diff --git a/Modules/IO/Extensions.Response.Content.cs b/Modules/IO/Extensions.Response.Content.cs index 051f74b05..2586aadcd 100644 --- a/Modules/IO/Extensions.Response.Content.cs +++ b/Modules/IO/Extensions.Response.Content.cs @@ -1,14 +1,13 @@ namespace GenHTTP.Modules.IO; -using GenHTTP.Api.Content.IO; -using Api.Protocol; - -using Streaming; - -using StreamContent = GenHTTP.Modules.IO.Streaming.StreamContent; +// using StreamContent = GenHTTP.Modules.IO.Streaming.StreamContent; public static class ResponseContentExtensions { + + // todo + + /* private static readonly FlexibleContentType TextPlainType = new(ContentType.TextPlain, "UTF-8"); /// @@ -48,5 +47,6 @@ public static class ResponseContentExtensions /// The data to be sent /// The logic to efficiently calculate checksums public static IResponseBuilder Content(this IResponseBuilder builder, ReadOnlyMemory data, Func>? checksumProvider = null) => builder.Content(new MemoryContent(data, checksumProvider)); + */ } diff --git a/Modules/IO/Extensions.Response.Type.cs b/Modules/IO/Extensions.Response.Type.cs index d90324dbc..4f9a725d2 100644 --- a/Modules/IO/Extensions.Response.Type.cs +++ b/Modules/IO/Extensions.Response.Type.cs @@ -1,10 +1,11 @@ -using GenHTTP.Api.Protocol; - -namespace GenHTTP.Modules.IO; +namespace GenHTTP.Modules.IO; public static class ResponseTypeExtensions { + // todo + + /* /// /// Specifies the content type of this response. /// @@ -16,5 +17,6 @@ public static class ResponseTypeExtensions /// /// The content type of this response public static IResponseBuilder Type(this IResponseBuilder builder, string contentType) => builder.Type(FlexibleContentType.Parse(contentType)); + */ } diff --git a/Modules/IO/FileSystem/FileResource.cs b/Modules/IO/FileSystem/FileResource.cs index 1d98aece4..8decc1612 100644 --- a/Modules/IO/FileSystem/FileResource.cs +++ b/Modules/IO/FileSystem/FileResource.cs @@ -1,4 +1,5 @@ -using GenHTTP.Api.Content.IO; +using System.Buffers; +using GenHTTP.Api.Content.IO; using GenHTTP.Api.Protocol; namespace GenHTTP.Modules.IO.FileSystem; @@ -56,6 +57,11 @@ public ulong? Length public ValueTask GetContentAsync() => new(File.OpenRead()); + public void Write(IBufferWriter writer) + { + throw new NotImplementedException(); + } + public ValueTask CalculateChecksumAsync() => new(Checksum.Calculate(this)); #endregion diff --git a/Modules/IO/Providers/ContentProvider.cs b/Modules/IO/Providers/ContentProvider.cs index 17892b01f..6811525ba 100644 --- a/Modules/IO/Providers/ContentProvider.cs +++ b/Modules/IO/Providers/ContentProvider.cs @@ -8,6 +8,9 @@ namespace GenHTTP.Modules.IO.Providers; public sealed class ContentProvider : IHandler { + private readonly ReadOnlyMemory _contentTypeName = "Content-Type"u8.ToArray(); + private readonly ReadOnlyMemory _contentTypeValue = "text/plain"u8.ToArray(); + #region Get-/Setters @@ -15,7 +18,7 @@ public sealed class ContentProvider : IHandler private IResponseContent Content { get; } - private FlexibleContentType ContentType { get; } + // private FlexibleContentType ContentType { get; } #endregion @@ -26,7 +29,8 @@ public ContentProvider(IResource resourceProvider) Resource = resourceProvider; Content = new ResourceContent(Resource); - ContentType = Resource.ContentType ?? FlexibleContentType.Get(Resource.Name?.GuessContentType() ?? Api.Protocol.ContentType.ApplicationForceDownload); + + // ContentType = Resource.ContentType ?? FlexibleContentType.Get(Resource.Name?.GuessContentType() ?? Api.Protocol.ContentType.ApplicationForceDownload); } #endregion @@ -36,16 +40,18 @@ public ContentProvider(IResource resourceProvider) public ValueTask HandleAsync(IRequest request) { var response = request.Respond() + .Raw() + .Status(ResponseStatus.Ok) .Content(Content) - .Type(ContentType); + .Header(_contentTypeName, _contentTypeValue); - if (Resource.Modified != null) + /*if (Resource.Modified != null) { response.Modified(Resource.Modified.Value); - } - + }/*/ + return new(response.Build()); - } + } public ValueTask PrepareAsync() => ValueTask.CompletedTask; diff --git a/Modules/IO/Providers/DownloadProvider.cs b/Modules/IO/Providers/DownloadProvider.cs index 17f8fadb8..1bf53cb9e 100644 --- a/Modules/IO/Providers/DownloadProvider.cs +++ b/Modules/IO/Providers/DownloadProvider.cs @@ -1,9 +1,6 @@ -using GenHTTP.Api.Content; -using GenHTTP.Api.Content.IO; -using GenHTTP.Api.Protocol; - -namespace GenHTTP.Modules.IO.Providers; +namespace GenHTTP.Modules.IO.Providers; +/* public sealed class DownloadProvider : IHandler { @@ -74,3 +71,4 @@ public DownloadProvider(IResource resourceProvider, string? fileName, FlexibleCo #endregion } +*/ diff --git a/Modules/IO/Providers/DownloadProviderBuilder.cs b/Modules/IO/Providers/DownloadProviderBuilder.cs index 4956e81ae..b954e820f 100644 --- a/Modules/IO/Providers/DownloadProviderBuilder.cs +++ b/Modules/IO/Providers/DownloadProviderBuilder.cs @@ -1,10 +1,6 @@ -using GenHTTP.Api.Content; -using GenHTTP.Api.Content.IO; -using GenHTTP.Api.Infrastructure; -using GenHTTP.Api.Protocol; - -namespace GenHTTP.Modules.IO.Providers; +namespace GenHTTP.Modules.IO.Providers; +/* public sealed class DownloadProviderBuilder : IHandlerBuilder { @@ -53,3 +49,4 @@ public IHandler Build() #endregion } +*/ diff --git a/Modules/IO/Providers/ResourceHandler.cs b/Modules/IO/Providers/ResourceHandler.cs index dcf3cc6c8..5f21fedfe 100644 --- a/Modules/IO/Providers/ResourceHandler.cs +++ b/Modules/IO/Providers/ResourceHandler.cs @@ -1,9 +1,6 @@ -using GenHTTP.Api.Content; -using GenHTTP.Api.Content.IO; -using GenHTTP.Api.Protocol; - -namespace GenHTTP.Modules.IO.Providers; +namespace GenHTTP.Modules.IO.Providers; +/* public sealed class ResourceHandler : IHandler { @@ -46,3 +43,4 @@ public ResourceHandler(IResourceTree tree) #endregion } +*/ diff --git a/Modules/IO/Providers/ResourceHandlerBuilder.cs b/Modules/IO/Providers/ResourceHandlerBuilder.cs index 53923292d..7fa2fe141 100644 --- a/Modules/IO/Providers/ResourceHandlerBuilder.cs +++ b/Modules/IO/Providers/ResourceHandlerBuilder.cs @@ -1,9 +1,6 @@ -using GenHTTP.Api.Content; -using GenHTTP.Api.Content.IO; -using GenHTTP.Api.Infrastructure; - -namespace GenHTTP.Modules.IO.Providers; +namespace GenHTTP.Modules.IO.Providers; +/* public sealed class ResourceHandlerBuilder : IHandlerBuilder { private readonly List _concerns = []; @@ -36,3 +33,4 @@ public IHandler Build() #endregion } +*/ diff --git a/Modules/IO/RangeSupport.cs b/Modules/IO/RangeSupport.cs index 516ecf036..8a676c5c8 100644 --- a/Modules/IO/RangeSupport.cs +++ b/Modules/IO/RangeSupport.cs @@ -1,9 +1,6 @@ -using GenHTTP.Api.Content; - -using GenHTTP.Modules.IO.Ranges; - -namespace GenHTTP.Modules.IO; +namespace GenHTTP.Modules.IO; +/* public static class RangeSupport { @@ -23,3 +20,4 @@ public static T AddRangeSupport(this T builder) where T : IHandlerBuilder #endregion } +*/ diff --git a/Modules/IO/Ranges/RangeSupportConcern.cs b/Modules/IO/Ranges/RangeSupportConcern.cs index 610ddc4ba..95630d697 100644 --- a/Modules/IO/Ranges/RangeSupportConcern.cs +++ b/Modules/IO/Ranges/RangeSupportConcern.cs @@ -1,9 +1,6 @@ -using System.Text.RegularExpressions; -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; - -namespace GenHTTP.Modules.IO.Ranges; +namespace GenHTTP.Modules.IO.Ranges; +/* public partial class RangeSupportConcern : IConcern { private static readonly Regex Pattern = CreatePattern(); @@ -134,7 +131,7 @@ private static IResponse NotSatisfiable(IRequest request, ulong totalLength) return request.Respond() .Status(ResponseStatus.RequestedRangeNotSatisfiable) - .Header("Content-Range", $"bytes */{totalLength}") + .Header("Content-Range", $"bytes /{totalLength}") // todo: add * back before the slash :) .Content(content) .Build(); } @@ -142,3 +139,4 @@ private static IResponse NotSatisfiable(IRequest request, ulong totalLength) #endregion } +*/ diff --git a/Modules/IO/Ranges/RangeSupportConcernBuilder.cs b/Modules/IO/Ranges/RangeSupportConcernBuilder.cs index c8c00c10b..86900079c 100644 --- a/Modules/IO/Ranges/RangeSupportConcernBuilder.cs +++ b/Modules/IO/Ranges/RangeSupportConcernBuilder.cs @@ -1,7 +1,6 @@ -using GenHTTP.Api.Content; - -namespace GenHTTP.Modules.IO.Ranges; +namespace GenHTTP.Modules.IO.Ranges; +/* public class RangeSupportConcernBuilder : IConcernBuilder { @@ -12,3 +11,4 @@ public class RangeSupportConcernBuilder : IConcernBuilder #endregion } +*/ diff --git a/Modules/IO/Ranges/RangedContent.cs b/Modules/IO/Ranges/RangedContent.cs index 8dc5eab2a..d7706684a 100644 --- a/Modules/IO/Ranges/RangedContent.cs +++ b/Modules/IO/Ranges/RangedContent.cs @@ -1,7 +1,6 @@ -using GenHTTP.Api.Protocol; - -namespace GenHTTP.Modules.IO.Ranges; +namespace GenHTTP.Modules.IO.Ranges; +/* public class RangedContent : IResponseContent { @@ -54,3 +53,4 @@ public RangedContent(IResponseContent source, ulong start, ulong end) #endregion } +*/ diff --git a/Modules/IO/Resource.cs b/Modules/IO/Resource.cs index 11d4fb480..87c0976f2 100644 --- a/Modules/IO/Resource.cs +++ b/Modules/IO/Resource.cs @@ -3,7 +3,6 @@ using GenHTTP.Modules.IO.Embedded; using GenHTTP.Modules.IO.FileSystem; using GenHTTP.Modules.IO.Strings; -using GenHTTP.Modules.IO.Web; namespace GenHTTP.Modules.IO; @@ -51,12 +50,12 @@ public static class Resource /// Creates a resource to be fetched from the given URL. /// /// The URI to fetch the resource from - public static WebResourceBuilder FromWeb(string source) => new WebResourceBuilder().Source(source); + // public static WebResourceBuilder FromWeb(string source) => new WebResourceBuilder().Source(source); /// /// Creates a resource to be fetched from the given URL. /// /// The URI to fetch the resource from - public static WebResourceBuilder FromWeb(Uri source) => new WebResourceBuilder().Source(source); + // public static WebResourceBuilder FromWeb(Uri source) => new WebResourceBuilder().Source(source); } diff --git a/Modules/IO/Resources.cs b/Modules/IO/Resources.cs index 9e33d4de1..13f4bc2a5 100644 --- a/Modules/IO/Resources.cs +++ b/Modules/IO/Resources.cs @@ -1,9 +1,6 @@ -using GenHTTP.Api.Content.IO; -using GenHTTP.Api.Infrastructure; -using GenHTTP.Modules.IO.Providers; - -namespace GenHTTP.Modules.IO; +namespace GenHTTP.Modules.IO; +/* /// /// Provides a folder structure (provided by a resource tree) to /// requesting clients. @@ -29,3 +26,4 @@ public static class Resources /// The resource tree to read resourced from public static ResourceHandlerBuilder From(IResourceTree tree) => new ResourceHandlerBuilder().Tree(tree); } +*/ diff --git a/Modules/IO/Streaming/ByteArrayContent.cs b/Modules/IO/Streaming/ByteArrayContent.cs index 24b943a17..62de34ce2 100644 --- a/Modules/IO/Streaming/ByteArrayContent.cs +++ b/Modules/IO/Streaming/ByteArrayContent.cs @@ -1,7 +1,6 @@ -using GenHTTP.Api.Protocol; - namespace GenHTTP.Modules.IO.Streaming; +/* /// /// Response content backed by a byte array. /// @@ -56,3 +55,4 @@ private static ulong CalculateChecksum(byte[] content) #endregion } +*/ diff --git a/Modules/IO/Streaming/MemoryContent.cs b/Modules/IO/Streaming/MemoryContent.cs index 012d1841b..5dfd4ca21 100644 --- a/Modules/IO/Streaming/MemoryContent.cs +++ b/Modules/IO/Streaming/MemoryContent.cs @@ -1,7 +1,6 @@ -using GenHTTP.Api.Protocol; - namespace GenHTTP.Modules.IO.Streaming; +/* /// /// Response content backed by a ReadOnlyMemory of bytes. /// @@ -56,3 +55,4 @@ private static ulong CalculateChecksum(ReadOnlySpan content) #endregion } +*/ diff --git a/Modules/IO/Streaming/ResourceContent.cs b/Modules/IO/Streaming/ResourceContent.cs index e4d1a0eac..5d78a13be 100644 --- a/Modules/IO/Streaming/ResourceContent.cs +++ b/Modules/IO/Streaming/ResourceContent.cs @@ -29,6 +29,12 @@ public ResourceContent(IResource resource) public ValueTask WriteAsync(Stream target, uint bufferSize) => Resource.WriteAsync(target, bufferSize); + public ValueTask WriteAsync(IResponseSink sink) + { + Resource.Write(sink.Writer); + return ValueTask.CompletedTask; + } + #endregion } diff --git a/Modules/IO/Streaming/StreamContent.cs b/Modules/IO/Streaming/StreamContent.cs index 4d59a5dd5..a088ac08f 100644 --- a/Modules/IO/Streaming/StreamContent.cs +++ b/Modules/IO/Streaming/StreamContent.cs @@ -1,7 +1,6 @@ -using GenHTTP.Api.Protocol; - -namespace GenHTTP.Modules.IO.Streaming; +namespace GenHTTP.Modules.IO.Streaming; +/* public sealed class StreamContent : IResponseContent, IDisposable { private readonly ChecksumProvider _checksumProvider; @@ -78,3 +77,4 @@ public void Dispose() #endregion } +*/ diff --git a/Modules/IO/Strings/StringContent.cs b/Modules/IO/Strings/StringContent.cs index d14b41cf7..334bf7ff8 100644 --- a/Modules/IO/Strings/StringContent.cs +++ b/Modules/IO/Strings/StringContent.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Buffers; +using System.Text; using GenHTTP.Api.Protocol; namespace GenHTTP.Modules.IO.Strings; @@ -32,11 +33,17 @@ public StringContent(string content) #region Functionality - public ValueTask CalculateChecksumAsync() => new(_checksum); + /*public ValueTask CalculateChecksumAsync() => new(_checksum); public async ValueTask WriteAsync(Stream target, uint bufferSize) { await target.WriteAsync(_content.AsMemory()); + }*/ + + public ValueTask WriteAsync(IResponseSink sink) + { + sink.Writer.Write(_content.AsSpan()); + return ValueTask.CompletedTask; } #endregion diff --git a/Modules/IO/Strings/StringResource.cs b/Modules/IO/Strings/StringResource.cs index b3940b86a..e810d751f 100644 --- a/Modules/IO/Strings/StringResource.cs +++ b/Modules/IO/Strings/StringResource.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Buffers; +using System.Text; using GenHTTP.Api.Content.IO; using GenHTTP.Api.Protocol; @@ -46,6 +47,11 @@ public StringResource(string content, string? name, FlexibleContentType? content public ValueTask WriteAsync(Stream target, uint bufferSize) => target.WriteAsync(Content.AsMemory()); + public void Write(IBufferWriter writer) + { + writer.Write(Content.AsSpan()); + } + #endregion } diff --git a/Modules/IO/Tracking/ChangeTrackingResource.cs b/Modules/IO/Tracking/ChangeTrackingResource.cs index 4f0dbf088..d17044827 100644 --- a/Modules/IO/Tracking/ChangeTrackingResource.cs +++ b/Modules/IO/Tracking/ChangeTrackingResource.cs @@ -1,4 +1,5 @@ -using GenHTTP.Api.Content.IO; +using System.Buffers; +using GenHTTP.Api.Content.IO; using GenHTTP.Api.Protocol; namespace GenHTTP.Modules.IO.Tracking; @@ -46,6 +47,11 @@ public async ValueTask WriteAsync(Stream target, uint bufferSize) await Source.WriteAsync(target, bufferSize); } + public void Write(IBufferWriter writer) + { + throw new NotImplementedException(); + } + public ValueTask CalculateChecksumAsync() => Source.CalculateChecksumAsync(); /// diff --git a/Modules/IO/Web/WebResource.cs b/Modules/IO/Web/WebResource.cs index bad37a0e9..7aae4fe3c 100644 --- a/Modules/IO/Web/WebResource.cs +++ b/Modules/IO/Web/WebResource.cs @@ -1,10 +1,6 @@ -using GenHTTP.Api.Content.IO; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.IO.Streaming; - -namespace GenHTTP.Modules.IO.Web; +namespace GenHTTP.Modules.IO.Web; +/* public class WebResource : IResource { private readonly HttpClient _client = new(); @@ -119,3 +115,4 @@ private void UpdateFields(HttpResponseMessage response) #endregion } +*/ diff --git a/Modules/IO/Web/WebResourceBuilder.cs b/Modules/IO/Web/WebResourceBuilder.cs index 84edf51fe..8f375961b 100644 --- a/Modules/IO/Web/WebResourceBuilder.cs +++ b/Modules/IO/Web/WebResourceBuilder.cs @@ -1,9 +1,6 @@ -using GenHTTP.Api.Content.IO; -using GenHTTP.Api.Infrastructure; -using GenHTTP.Api.Protocol; - -namespace GenHTTP.Modules.IO.Web; +namespace GenHTTP.Modules.IO.Web; +/* public class WebResourceBuilder : IResourceBuilder { private Uri? _source; @@ -74,3 +71,4 @@ public IResource Build() #endregion } +*/ diff --git a/Modules/Layouting/MultiSegmentSupport.cs b/Modules/Layouting/MultiSegmentSupport.cs index 78f334b00..7b5248358 100644 --- a/Modules/Layouting/MultiSegmentSupport.cs +++ b/Modules/Layouting/MultiSegmentSupport.cs @@ -48,7 +48,7 @@ public static LayoutBuilder AddSegments(this LayoutBuilder builder, params strin foreach (var section in segments) { - if (current.RoutedHandlers.TryGetValue(section, out var existing)) + if (current.RoutedHandlers.TryGetValue(section.Hash(), out var existing)) { if (existing is LayoutBuilder existingLayout) { diff --git a/Modules/Layouting/Provider/LayoutBuilder.cs b/Modules/Layouting/Provider/LayoutBuilder.cs index 5a0b4e091..a992ef3f7 100644 --- a/Modules/Layouting/Provider/LayoutBuilder.cs +++ b/Modules/Layouting/Provider/LayoutBuilder.cs @@ -10,7 +10,7 @@ public sealed class LayoutBuilder : IHandlerBuilder #region Get-/Setters - internal Dictionary RoutedHandlers { get; } = []; + internal Dictionary RoutedHandlers { get; } = []; internal List RootHandlers { get; } = []; @@ -57,7 +57,7 @@ public LayoutBuilder Add(string name, IHandlerBuilder handler) return this.Add(name.Split('/', StringSplitOptions.RemoveEmptyEntries), handler); } - if (!RoutedHandlers.TryAdd(name, handler)) + if (!RoutedHandlers.TryAdd(name.Hash(), handler)) { throw new InvalidOperationException($"A segment with the name '{name}' has already been added to the layout"); } @@ -118,7 +118,7 @@ public IHandler Build() var routed = RoutedHandlers.ToDictionary(kv => kv.Key, kv => kv.Value.Build()); var root = RootHandlers.Select(h => h.Build()).ToList(); - return Concerns.Chain(_concerns, new LayoutRouter(routed, root, _index?.Build())); + return Concerns.Chain(_concerns, new LayoutHandler(routed, root, _index?.Build())); } #endregion diff --git a/Modules/Layouting/Provider/LayoutHandler.cs b/Modules/Layouting/Provider/LayoutHandler.cs new file mode 100644 index 000000000..f72d99048 --- /dev/null +++ b/Modules/Layouting/Provider/LayoutHandler.cs @@ -0,0 +1,94 @@ +using System.Collections.Frozen; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +namespace GenHTTP.Modules.Layouting.Provider; + +public sealed class LayoutHandler : IHandler +{ + + #region Get-/Setters + + public FrozenDictionary RoutedHandlers { get; } + + public IHandler[] RootHandlers { get; } + + public IHandler? Index { get; } + + #endregion + + #region Initialization + + public LayoutHandler(Dictionary routedHandlers, List rootHandlers, IHandler? index) + { + RoutedHandlers = routedHandlers.ToFrozenDictionary(); + RootHandlers = rootHandlers.ToArray(); + Index = index; + } + + #endregion + + #region Functionality + + public async ValueTask PrepareAsync() + { + // todo: parallelize? + + foreach (var routed in RoutedHandlers.Values) + { + await routed.PrepareAsync(); + } + + foreach (var root in RootHandlers) + { + await root.PrepareAsync(); + } + + if (Index != null) + { + await Index.PrepareAsync(); + } + } + + public ValueTask HandleAsync(IRequest request) + { + var target = request.Raw.Header.Target; + + if (target.Current is not null) + { + var hash = target.Current.Value.Hash(); + + if (RoutedHandlers.TryGetValue(hash, out var handler)) + { + target.Advance(); + + return handler.HandleAsync(request); + } + } + else if (Index is not null) + { + return Index.HandleAsync(request); + } + + return InvokeRootHandlersAsync(request); + } + + private async ValueTask InvokeRootHandlersAsync(IRequest request) + { + foreach (var handler in RootHandlers) + { + var result = await handler.HandleAsync(request); + + if (result != null) + { + return result; + } + } + + return null; + } + + #endregion + +} diff --git a/Modules/Layouting/Provider/LayoutRouter.cs b/Modules/Layouting/Provider/LayoutRouter.cs deleted file mode 100644 index d6144de68..000000000 --- a/Modules/Layouting/Provider/LayoutRouter.cs +++ /dev/null @@ -1,101 +0,0 @@ -using GenHTTP.Api.Content; -using GenHTTP.Api.Protocol; - -using GenHTTP.Modules.Redirects; - -namespace GenHTTP.Modules.Layouting.Provider; - -public sealed class LayoutRouter : IHandler -{ - - #region Get-/Setters - - public IReadOnlyDictionary RoutedHandlers { get; } - - public IHandler[] RootHandlers { get; } - - public IHandler? Index { get; } - - #endregion - - #region Initialization - - public LayoutRouter(Dictionary routedHandlers, List rootHandlers, IHandler? index) - { - RoutedHandlers = routedHandlers; - RootHandlers = rootHandlers.ToArray(); - Index = index; - } - - #endregion - - #region Functionality - - public ValueTask HandleAsync(IRequest request) - { - var current = request.Target.Current; - - if (current is not null) - { - if (RoutedHandlers.TryGetValue(current.Value, out var handler)) - { - request.Target.Advance(); - - return handler.HandleAsync(request); - } - } - else - { - // force a trailing slash to prevent duplicate content - if (!request.Target.Path.TrailingSlash) - { - return Redirect.To($"{request.Target.Path}/") - .Build() - .HandleAsync(request); - } - - if (Index is not null) - { - return Index.HandleAsync(request); - } - } - - return InvokeRootHandlersAsync(request); - } - - public async ValueTask PrepareAsync() - { - if (Index != null) - { - await Index.PrepareAsync(); - } - - foreach (var handler in RoutedHandlers.Values) - { - await handler.PrepareAsync(); - } - - foreach (var handler in RootHandlers) - { - await handler.PrepareAsync(); - } - } - - private async ValueTask InvokeRootHandlersAsync(IRequest request) - { - foreach (var handler in RootHandlers) - { - var result = await handler.HandleAsync(request); - - if (result != null) - { - return result; - } - } - - return null; - } - - #endregion - -} diff --git a/Modules/Layouting/Provider/RawUtils.cs b/Modules/Layouting/Provider/RawUtils.cs new file mode 100644 index 000000000..c22bbc516 --- /dev/null +++ b/Modules/Layouting/Provider/RawUtils.cs @@ -0,0 +1,30 @@ +using System.Runtime.CompilerServices; +using System.Text; + +namespace GenHTTP.Modules.Layouting.Provider; + +// todo: where to put this? + +public static class RawUtils +{ + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Hash(this ReadOnlyMemory data) + { + const uint fnvOffset = 2166136261; + const uint fnvPrime = 16777619; + + var hash = fnvOffset; + + foreach (var b in data.Span) + { + hash ^= b; + hash *= fnvPrime; + } + + return unchecked((int)hash); + } + + public static int Hash(this string data) => Hash(Encoding.ASCII.GetBytes(data)); + +} diff --git a/Modules/Pages/Extensions.cs b/Modules/Pages/Extensions.cs index 9a24cd4c3..f99a77bbf 100644 --- a/Modules/Pages/Extensions.cs +++ b/Modules/Pages/Extensions.cs @@ -1,12 +1,16 @@ using System.Web; + using GenHTTP.Api.Protocol; + using StringContent = GenHTTP.Modules.IO.Strings.StringContent; namespace GenHTTP.Modules.Pages; public static class Extensions { - private static readonly FlexibleContentType ContentType = new(Api.Protocol.ContentType.TextHtml, "utf-8"); + // todo + private static readonly ReadOnlyMemory ContentTypeHeader = "Content-Type"u8.ToArray(); + private static readonly ReadOnlyMemory ContentTypeValue = "text/html; charset=\"utf-8\""u8.ToArray(); /// /// Creates a response that can be returned by a handler to serve @@ -16,8 +20,10 @@ public static class Extensions /// The HTML page to be served /// The HTML page response public static IResponseBuilder GetPage(this IRequest request, string content) => request.Respond() + .Raw() .Content(new StringContent(content)) - .Type(ContentType); + .Header(ContentTypeHeader, ContentTypeValue) + .Unraw(); /// /// Escapes the given string so it can safely be used in HTML. @@ -25,4 +31,5 @@ public static IResponseBuilder GetPage(this IRequest request, string content) => /// The content to be escaped /// The escaped version of the string public static string Escaped(this string content) => HttpUtility.HtmlEncode(content); + } diff --git a/Modules/Redirects/Provider/RedirectProvider.cs b/Modules/Redirects/Provider/RedirectProvider.cs index db5a7d610..e80315c84 100644 --- a/Modules/Redirects/Provider/RedirectProvider.cs +++ b/Modules/Redirects/Provider/RedirectProvider.cs @@ -1,19 +1,18 @@ -using System.Text.RegularExpressions; - -using GenHTTP.Api.Content; +using GenHTTP.Api.Content; using GenHTTP.Api.Protocol; using GenHTTP.Modules.IO; namespace GenHTTP.Modules.Redirects.Provider; -public sealed partial class RedirectProvider : IHandler +public sealed class RedirectProvider : IHandler { - private static readonly Regex ProtocolMatcher = CreateProtocolMatcher(); #region Get-/Setters - public string Target { get; } + public Uri Target { get; } + + private string StringTarget { get; } public bool Temporary { get; } @@ -21,15 +20,14 @@ public sealed partial class RedirectProvider : IHandler #region Initialization - public RedirectProvider(string location, bool temporary) + public RedirectProvider(Uri location, bool temporary) { Target = location; + StringTarget = location.ToString(); + Temporary = temporary; } - [GeneratedRegex("^[a-z_-]+://", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-US")] - private static partial Regex CreateProtocolMatcher(); - #endregion #region Functionality @@ -38,26 +36,27 @@ public RedirectProvider(string location, bool temporary) public ValueTask HandleAsync(IRequest request) { - var resolved = ResolveRoute(request, Target); - - var response = request.Respond() - .Header("Location", resolved); + var resolved = ResolveRoute(request); var status = MapStatus(request, Temporary); - return new ValueTask(response.Status(status).Build()); + var response = request.Respond() + .Header("Location", resolved) + .Status(status); + + return new ValueTask(response.Build()); } - private static string ResolveRoute(IRequest request, string route) + private string ResolveRoute(IRequest request) { - if (ProtocolMatcher.IsMatch(route)) + if (Target.IsAbsoluteUri) { - return route; + return StringTarget; } - var protocol = request.EndPoint.Secure ? "https://" : "http://"; + // todo: var protocol = request.EndPoint.Secure ? "https://" : "http://"; - return $"{protocol}{request.Host}{route}"; + return $"http://{request.Host}{StringTarget}"; } private static ResponseStatus MapStatus(IRequest request, bool temporary) @@ -66,6 +65,7 @@ private static ResponseStatus MapStatus(IRequest request, bool temporary) { return temporary ? ResponseStatus.TemporaryRedirect : ResponseStatus.MovedPermanently; } + return temporary ? ResponseStatus.SeeOther : ResponseStatus.PermanentRedirect; } diff --git a/Modules/Redirects/Provider/RedirectProviderBuilder.cs b/Modules/Redirects/Provider/RedirectProviderBuilder.cs index 573774836..cd66cf189 100644 --- a/Modules/Redirects/Provider/RedirectProviderBuilder.cs +++ b/Modules/Redirects/Provider/RedirectProviderBuilder.cs @@ -37,7 +37,12 @@ public IHandler Build() throw new BuilderMissingPropertyException("Location"); } - return Concerns.Chain(_concerns, new RedirectProvider(_location, _temporary)); + if (Uri.TryCreate(_location, UriKind.RelativeOrAbsolute, out var parsed)) + { + return Concerns.Chain(_concerns, new RedirectProvider(parsed, _temporary)); + } + + throw new InvalidOperationException("The given location is not a valid URI"); } #endregion diff --git a/Modules/ReverseProxy/GenHTTP.Modules.ReverseProxy.csproj b/Modules/ReverseProxy/GenHTTP.Modules.ReverseProxy.csproj index 41f3f246d..2eb8337b1 100644 --- a/Modules/ReverseProxy/GenHTTP.Modules.ReverseProxy.csproj +++ b/Modules/ReverseProxy/GenHTTP.Modules.ReverseProxy.csproj @@ -18,8 +18,6 @@ - - diff --git a/Modules/Websockets/GenHTTP.Modules.Websockets.csproj b/Modules/Websockets/GenHTTP.Modules.Websockets.csproj index c5a523810..f160052e2 100644 --- a/Modules/Websockets/GenHTTP.Modules.Websockets.csproj +++ b/Modules/Websockets/GenHTTP.Modules.Websockets.csproj @@ -12,8 +12,6 @@ - - diff --git a/Packages/GenHTTP.Core.nuspec b/Packages/GenHTTP.Core.nuspec index 51dce297d..774eb512b 100644 --- a/Packages/GenHTTP.Core.nuspec +++ b/Packages/GenHTTP.Core.nuspec @@ -3,7 +3,7 @@ GenHTTP.Core - 11.0.0 + 11.0.0-preview.6 Basic dependencies for projects using the GenHTTP framework, including the engine itself @@ -22,13 +22,14 @@ - + - + - - - + + + + diff --git a/Playground/ChunkedJsonContent.cs b/Playground/ChunkedJsonContent.cs new file mode 100644 index 000000000..e107a7365 --- /dev/null +++ b/Playground/ChunkedJsonContent.cs @@ -0,0 +1,25 @@ +using System.Text.Json; + +using GenHTTP.Api.Protocol; + +namespace GenHTTP.Playground; + +public sealed class ChunkedJsonContent(JsonResult result) : IResponseContent +{ + private static readonly JsonWriterOptions Options = new() + { + SkipValidation = true + }; + + public ulong? Length => null; + + public ValueTask WriteAsync(IResponseSink sink) + { + using var writer = new Utf8JsonWriter(sink.Writer, Options); + + JsonSerializer.Serialize(writer, result); + + return ValueTask.CompletedTask; + } + +} \ No newline at end of file diff --git a/Playground/ChunkedJsonHandler.cs b/Playground/ChunkedJsonHandler.cs new file mode 100644 index 000000000..aecc57803 --- /dev/null +++ b/Playground/ChunkedJsonHandler.cs @@ -0,0 +1,30 @@ +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +namespace GenHTTP.Playground; + +public sealed class ChunkedJsonHandler : IHandler +{ + private static readonly ReadOnlyMemory ContentTypeName = "Content-Type"u8.ToArray(); + private static readonly ReadOnlyMemory ContentTypeValue = "application/json; charset=utf-8"u8.ToArray(); + + public ValueTask PrepareAsync() => new(); + + public ValueTask HandleAsync(IRequest request) + { + var result = new JsonResult() + { + Message = "Hello, World!" + }; + + var response = request.Respond() + .Raw() + .Status(ResponseStatus.Ok) + .Content(new ChunkedJsonContent(result)) + .Header(ContentTypeName, ContentTypeValue) + .Build(); + + return new(response); + } + +} diff --git a/Playground/FixedJsonHandler.cs b/Playground/FixedJsonHandler.cs new file mode 100644 index 000000000..989b8b283 --- /dev/null +++ b/Playground/FixedJsonHandler.cs @@ -0,0 +1,37 @@ +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +namespace GenHTTP.Playground; + +public sealed class JsonResult +{ + + public string? Message { get; set; } +} + +public sealed class FixedJsonHandler : IHandler +{ + private static readonly ReadOnlyMemory OkPhrase = "OK"u8.ToArray(); + private static readonly ReadOnlyMemory ContentTypeName = "Content-Type"u8.ToArray(); + private static readonly ReadOnlyMemory ContentTypeValue = "application/json; charset=utf-8"u8.ToArray(); + + public ValueTask PrepareAsync() => new(); + + public ValueTask HandleAsync(IRequest request) + { + var result = new JsonResult() + { + Message = "Hello, World!" + }; + + var response = request.Respond() + .Raw() + .Status(ResponseStatus.Ok) + .Content(new FixedLengthJsonContent(result)) + .Header(ContentTypeName, ContentTypeValue) + .Build(); + + return new(response); + } + +} diff --git a/Playground/FixedLengthJsonContent.cs b/Playground/FixedLengthJsonContent.cs new file mode 100644 index 000000000..da8d577b8 --- /dev/null +++ b/Playground/FixedLengthJsonContent.cs @@ -0,0 +1,29 @@ +using System.Buffers; +using System.Text.Json; +using GenHTTP.Api.Protocol; + +namespace GenHTTP.Playground; + +public sealed class FixedLengthJsonContent : IResponseContent +{ + private readonly byte[] _buffer; + + public ulong? Length => (ulong)_buffer.Length; + + public FixedLengthJsonContent(JsonResult result) + { + using var ms = new MemoryStream(27); + JsonSerializer.Serialize(ms, result); + + ms.TryGetBuffer(out var segment); + + _buffer = segment.Array!; + } + + public ValueTask WriteAsync(IResponseSink sink) + { + sink.Writer.Write(_buffer); + return ValueTask.CompletedTask; + } + +} \ No newline at end of file diff --git a/Playground/GenHTTP.Playground.csproj b/Playground/GenHTTP.Playground.csproj index a267f8ca5..cf176c9a3 100644 --- a/Playground/GenHTTP.Playground.csproj +++ b/Playground/GenHTTP.Playground.csproj @@ -14,9 +14,14 @@ - + - + + + + + + diff --git a/Playground/Program.cs b/Playground/Program.cs index 6a603cc9a..cfd585a1c 100644 --- a/Playground/Program.cs +++ b/Playground/Program.cs @@ -1,17 +1,15 @@ using GenHTTP.Engine.Internal; -using GenHTTP.Modules.Archives; -using GenHTTP.Modules.DirectoryBrowsing; using GenHTTP.Modules.IO; -using GenHTTP.Modules.Practices; +using GenHTTP.Modules.Layouting; +using GenHTTP.Playground; -var archive = Resource.FromWeb("https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.102/dotnet-sdk-10.0.102-linux-x64.tar.gz"); - -var tree = ArchiveTree.From(archive); - -var listing = Listing.From(tree); +var app = Layout.Create() + .Add("plaintext", Content.From(Resource.FromString("Hello World!"))) + .Add("jsonf", new FixedJsonHandler()) + .Add("json", new ChunkedJsonHandler()); await Host.Create() - .Handler(listing) - .Defaults() + .Handler(app) + // .Defaults() .Console() .RunAsync();