diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0111fda6..357fb8a0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: - 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/**/*,**/*.css,**/*.js,**/*.html" /d:sonar.coverage.exclusions="**/Engine/Ioxide/Tls/TlsDuplexPipe.cs" /o:"kaliumhexacyanoferrat" /k:"GenHTTP" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.branch.name="${GITHUB_REF##*/}" /d:sonar.dotnet.excludeTestProjects=true + run: dotnet sonarscanner begin /k:"GenHTTP" /d:sonar.token="$SONAR_TOKEN" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.exclusions="**/bin/**/*,**/obj/**/*,**/*.css,**/*.js,**/*.html" /d:sonar.coverage.exclusions="**/Engine/Ioxide/Tls/TlsDuplexPipe.cs" /d:sonar.cpd.exclusions="**/Generators/**/CodeEmitter.cs" /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 diff --git a/Engine/Ioxide/Protocol/DateHeader.cs b/Engine/Ioxide/Protocol/DateHeader.cs index d6d5db88..dbd1ba2c 100644 --- a/Engine/Ioxide/Protocol/DateHeader.cs +++ b/Engine/Ioxide/Protocol/DateHeader.cs @@ -17,7 +17,7 @@ internal static class DateHeader private static int _second; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ReadOnlySpan Get() + public static ReadOnlyMemory Get() { var buffer = _buffer; diff --git a/Engine/Ioxide/Protocol/ResponseWriter.cs b/Engine/Ioxide/Protocol/ResponseWriter.cs index fff96c00..c6730009 100644 --- a/Engine/Ioxide/Protocol/ResponseWriter.cs +++ b/Engine/Ioxide/Protocol/ResponseWriter.cs @@ -1,15 +1,17 @@ using System.Buffers; -using System.Buffers.Text; using System.IO.Pipelines; using GenHTTP.Api.Protocol; +using GenHTTP.Engine.Shared.Types; + namespace GenHTTP.Engine.Ioxide.Protocol; /// -/// Writes an to a . Forked from GenHTTP's -/// ResponseHandler; emits its own Server/Date headers. Fixed-length content is sent with a -/// Content-Length; unknown-length content is chunk-encoded (see ). +/// Writes an to a . Status line and body writing +/// stay engine-specific (Ioxide sinks are allocated fresh per response rather than pooled on a +/// per-connection context), but header serialization is shared with the Internal engine via +/// ; only the Server/Date header values differ. /// internal static class ResponseWriter { @@ -19,116 +21,16 @@ internal static async ValueTask WriteAsync(PipeWriter writer, IRequest? request, { writer.Write(StatusLine.Get(response.Status)); - WriteHeader(writer, response, keepAlive); + ResponseSerializer.WriteHeader(writer, response, keepAlive, ServerHeader, DateHeader.Get(), isHttp10: false); writer.Write("\r\n"u8); - if (ShouldSendBody(request, response, headRequest)) + if (ResponseSerializer.ShouldSendBody(request, response, headRequest)) { await WriteBodyAsync(writer, response); } } - private static bool ShouldSendBody(IRequest? request, IResponse response, bool headRequest) - { - if (request == null) - { - return true; - } - - if (headRequest) - { - return false; - } - - var content = response.Content; - - if (content != null) - { - return (content.Length ?? 1) > 0; - } - - return false; - } - - private static void WriteHeader(PipeWriter writer, IResponse response, bool keepAlive) - { - var isUpgrade = response.Mode == Connection.Upgrade; - - if (!response.Headers.ContainsKey(KnownHeaders.Server)) - { - writer.Write(ServerHeader); - } - - if (!response.Headers.ContainsKey(KnownHeaders.Date)) - { - writer.Write(DateHeader.Get()); - } - - if (isUpgrade) - { - writer.Write("Connection: Upgrade\r\n"u8); - } - else if (!keepAlive) - { - // HTTP/1.1 connections are persistent by default so we do not need to send a Keep-Alive header - writer.Write("Connection: Close\r\n"u8); - } - - var content = response.Content; - - if (content != null) - { - var type = content.Type; - - if (type != null) - { - writer.Write("Content-Type: "u8); - writer.Write(type.Value.Bytes.Span); - writer.Write("\r\n"u8); - } - - var length = content.Length; - - if (length != null) - { - writer.Write("Content-Length: "u8); - WriteNumber(writer, length.Value); - writer.Write("\r\n"u8); - } - else if (!isUpgrade) - { - // Unknown length: chunk-encode the body (keep-alive stays intact). - writer.Write("Transfer-Encoding: chunked\r\n"u8); - } - - var encoding = content.Encoding; - - if (encoding != null) - { - writer.Write("Content-Encoding: "u8); - writer.Write(encoding.Value.Span); - writer.Write("\r\n"u8); - } - } - else - { - writer.Write("Content-Length: 0\r\n"u8); - } - - var headers = response.Headers; - - for (var i = 0; i < headers.Count; i++) - { - var header = headers.GetMemoryEntry(i); - - writer.Write(header.Key.Span); - writer.Write(": "u8); - writer.Write(header.Value.Span); - writer.Write("\r\n"u8); - } - } - private static async ValueTask WriteBodyAsync(PipeWriter writer, IResponse response) { var content = response.Content; @@ -156,11 +58,4 @@ private static async ValueTask WriteBodyAsync(PipeWriter writer, IResponse respo } } - private static void WriteNumber(PipeWriter writer, ulong value) - { - var span = writer.GetSpan(20); - Utf8Formatter.TryFormat(value, span, out var written); - writer.Advance(written); - } - } diff --git a/Engine/Shared/Types/ResponseHandler.cs b/Engine/Shared/Types/ResponseHandler.cs index 3edd4378..eb25c983 100644 --- a/Engine/Shared/Types/ResponseHandler.cs +++ b/Engine/Shared/Types/ResponseHandler.cs @@ -42,7 +42,7 @@ public async ValueTask HandleAsync(IRequest? request, IResponse response, writer.Write("\r\n"u8); - if (ShouldSendBody(request, response, headRequest)) + if (ResponseSerializer.ShouldSendBody(request, response, headRequest)) { await WriteBodyAsync(response); } @@ -60,111 +60,11 @@ public async ValueTask HandleAsync(IRequest? request, IResponse response, } } - private static bool ShouldSendBody(IRequest? request, IResponse response, bool headRequest) - { - if (request == null) - { - return true; - } - - if (headRequest) - { - return false; - } - - var content = response.Content; - - if (content != null) - { - return (content.Length ?? 1) > 0; - } - - return false; - } - private void WriteHeader(IResponse response, HttpProtocol version, bool keepAlive) { var context = Context; - var writer = context.Writer; - - var isUpgrade = response.Mode == Connection.Upgrade; - - if (!response.Headers.ContainsKey(KnownHeaders.Server)) - { - writer.Write(ServerHeader.GetValue(context).Span); - } - - if (!response.Headers.ContainsKey(KnownHeaders.Date)) - { - writer.Write(DateHeader.GetValue().Span); - } - - if (isUpgrade) - { - writer.Write("Connection: Upgrade\r\n"u8); - } - else if (version == HttpProtocol.Http10) - { - writer.Write(keepAlive ? "Connection: Keep-Alive\r\n"u8 : "Connection: Close\r\n"u8); - } - else if (!keepAlive) - { - // HTTP/1.1 connections are persistent by default so we do not need to send a Keep-Alive header - writer.Write("Connection: Close\r\n"u8); - } - - var content = response.Content; - - if (content != null) - { - var type = content.Type; - - if (type != null) - { - writer.Write("Content-Type: "u8); - writer.Write(type.Value.Bytes.Span); - writer.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 if (!isUpgrade) - { - writer.Write("Transfer-Encoding: chunked\r\n"u8); - } - - var encoding = content.Encoding; - - if (encoding != null) - { - writer.Write("Content-Encoding: "u8); - writer.Write(encoding.Value.Span); - writer.Write("\r\n"u8); - } - } - else - { - writer.Write("Content-Length: 0\r\n"u8); - } - - var headers = response.Headers; - - for (var i = 0; i < headers.Count; i++) - { - var header = headers.GetMemoryEntry(i); - - writer.Write(header.Key.Span); - writer.Write(": "u8); - writer.Write(header.Value.Span); - writer.Write("\r\n"u8); - } + ResponseSerializer.WriteHeader(context.Writer, response, keepAlive, ServerHeader.GetValue(context), DateHeader.GetValue(), version == HttpProtocol.Http10); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Engine/Shared/Types/ResponseSerializer.cs b/Engine/Shared/Types/ResponseSerializer.cs new file mode 100644 index 00000000..32b81a68 --- /dev/null +++ b/Engine/Shared/Types/ResponseSerializer.cs @@ -0,0 +1,114 @@ +using System.Buffers; +using System.IO.Pipelines; + +using GenHTTP.Api.Protocol; + +namespace GenHTTP.Engine.Shared.Types; + +public static class ResponseSerializer +{ + + public static bool ShouldSendBody(IRequest? request, IResponse response, bool headRequest) + { + if (request == null) + { + return true; + } + + if (headRequest) + { + return false; + } + + var content = response.Content; + + if (content != null) + { + return (content.Length ?? 1) > 0; + } + + return false; + } + + public static void WriteHeader(PipeWriter writer, IResponse response, bool keepAlive, ReadOnlyMemory serverHeader, ReadOnlyMemory dateHeader, bool isHttp10) + { + var isUpgrade = response.Mode == Connection.Upgrade; + + if (!response.Headers.ContainsKey(KnownHeaders.Server)) + { + writer.Write(serverHeader.Span); + } + + if (!response.Headers.ContainsKey(KnownHeaders.Date)) + { + writer.Write(dateHeader.Span); + } + + if (isUpgrade) + { + writer.Write("Connection: Upgrade\r\n"u8); + } + else if (isHttp10) + { + writer.Write(keepAlive ? "Connection: Keep-Alive\r\n"u8 : "Connection: Close\r\n"u8); + } + else if (!keepAlive) + { + // HTTP/1.1 connections are persistent by default so we do not need to send a Keep-Alive header + writer.Write("Connection: Close\r\n"u8); + } + + var content = response.Content; + + if (content != null) + { + var type = content.Type; + + if (type != null) + { + writer.Write("Content-Type: "u8); + writer.Write(type.Value.Bytes.Span); + writer.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 if (!isUpgrade) + { + writer.Write("Transfer-Encoding: chunked\r\n"u8); + } + + var encoding = content.Encoding; + + if (encoding != null) + { + writer.Write("Content-Encoding: "u8); + writer.Write(encoding.Value.Span); + writer.Write("\r\n"u8); + } + } + else + { + writer.Write("Content-Length: 0\r\n"u8); + } + + var headers = response.Headers; + + for (var i = 0; i < headers.Count; i++) + { + var header = headers.GetMemoryEntry(i); + + writer.Write(header.Key.Span); + writer.Write(": "u8); + writer.Write(header.Value.Span); + writer.Write("\r\n"u8); + } + } + +}