Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Engine/Ioxide/Protocol/DateHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal static class DateHeader
private static int _second;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ReadOnlySpan<byte> Get()
public static ReadOnlyMemory<byte> Get()
{
var buffer = _buffer;

Expand Down
121 changes: 8 additions & 113 deletions Engine/Ioxide/Protocol/ResponseWriter.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Writes an <see cref="IResponse"/> to a <see cref="PipeWriter"/>. 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 <see cref="ChunkedWriter"/>).
/// Writes an <see cref="IResponse"/> to a <see cref="PipeWriter"/>. 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
/// <see cref="ResponseSerializer"/>; only the Server/Date header values differ.
/// </summary>
internal static class ResponseWriter
{
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}

}
104 changes: 2 additions & 102 deletions Engine/Shared/Types/ResponseHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public async ValueTask<bool> HandleAsync(IRequest? request, IResponse response,

writer.Write("\r\n"u8);

if (ShouldSendBody(request, response, headRequest))
if (ResponseSerializer.ShouldSendBody(request, response, headRequest))
{
await WriteBodyAsync(response);
}
Expand All @@ -60,111 +60,11 @@ public async ValueTask<bool> 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)]
Expand Down
Loading
Loading