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
32 changes: 22 additions & 10 deletions Engine/Internal/Infrastructure/Endpoints/EndPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,27 +98,39 @@ private async Task Listen()
{
if (Socket == null) throw new InvalidOperationException("The endpoint has not been started");

try
while (!_shuttingDown)
{
do
try
{
Handle(await Socket.AcceptAsync());
}
while (!_shuttingDown);
}
catch (Exception e)
{
if (!_shuttingDown && !ConnectionExceptions.IsGracefulDisconnect(e))
catch (ObjectDisposedException)
{
Logger.LogError(e, "Failed to accept incoming connection");
break;
}
catch (OperationCanceledException)
{
break;
}
catch (Exception e)
{
if (_shuttingDown)
{
break;
}

if (!ConnectionExceptions.IsGracefulDisconnect(e))
{
Logger.LogError(e, "Failed to accept incoming connection");
}

await Task.Delay(500);
}
}
}

private void Handle(Socket client)
{
using var _ = ExecutionContext.SuppressFlow();

Task.Run(() => Accept(client));
}

Expand Down
4 changes: 3 additions & 1 deletion Engine/Ioxide/GenHTTP.Engine.Ioxide.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
<ProjectReference Include="..\..\API\Ioxide\GenHTTP.Api.Ioxide.csproj" />

<ProjectReference Include="..\Shared\GenHTTP.Engine.Shared.csproj" />


<ProjectReference Include="..\..\Modules\IO\GenHTTP.Modules.IO.csproj" />

<PackageReference Include="ioxide" Version="0.13.225" />

<PackageReference Include="ioxide.http2" Version="0.13.225" />
Expand Down
29 changes: 29 additions & 0 deletions Engine/Ioxide/Protocol/Drivers/Tcp/Http1Driver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

using GenHTTP.Engine.Shared.Types;

using Glyph11;
using Glyph11.Parser;
using Glyph11.Parser.UltraHardened;
using Glyph11.Pico;
Expand All @@ -18,6 +19,8 @@
using IoConnection = ioxide.TcpConnection;
using GenHTTP.Engine.Ioxide.Protocol.Responses;

using StringContent = GenHTTP.Modules.IO.Strings.StringContent;

namespace GenHTTP.Engine.Ioxide.Protocol.Drivers.Tcp;

/// <summary>Serves HTTP/1.1 on one connection, request after request.</summary>
Expand Down Expand Up @@ -105,6 +108,10 @@ internal static async Task RunAsync(IServer server, IEndPoint endPoint, IDuplexP
}
}
}
catch (HttpParseException pe)
{
await SendErrorAsync(server, writer, pe, (ResponseStatus)pe.StatusCode);
}
finally
{
WarnIfThreadHopped(server, reactorThreadId, "before-return");
Expand All @@ -115,6 +122,28 @@ internal static async Task RunAsync(IServer server, IEndPoint endPoint, IDuplexP
}
}

private static async ValueTask SendErrorAsync(IServer server, PipeWriter writer, Exception e, ResponseStatus status)
{
try
{
var message = server.Development ? e.ToString() : e.Message;

var response = new ResponseBuilder()
.Status(status)
.Connection(Connection.Close)
.Content(new StringContent(message))
.Build();

await Http1Responder.WriteAsync(writer, null, response, keepAlive: false, headRequest: false);

await writer.FlushAsync();
}
catch
{
/* no recovery here */
}
}

// Parses one request head, through whichever parser this process was started with.
private static bool TryParseRequest(ref ReadOnlySequence<byte> buffer, BinaryRequest into)
=> UsePico ? TryParseRequestPico(ref buffer, into) : TryParseRequestGlyph11(ref buffer, into);
Expand Down
2 changes: 1 addition & 1 deletion Engine/Shared/Types/Body/ChunkedBodyStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public override async ValueTask<int> ReadAsync(Memory<byte> buffer, Cancellation

if (result.IsCompleted)
{
throw new InvalidDataException("Unexpected end of chunked body");
throw new ProviderException(ResponseStatus.BadRequest, "Unexpected end of chunked body");
}

continue;
Expand Down
22 changes: 15 additions & 7 deletions Engine/Shared/Types/Body/MemoryConsumptionStrategy.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Buffers;
using System.IO.Pipelines;
using GenHTTP.Api.Content;
using GenHTTP.Api.Protocol;

namespace GenHTTP.Engine.Shared.Types.Body;

Expand Down Expand Up @@ -41,7 +43,7 @@
{
return new(_memory.Value);
}

if (_length is not null)
{
return ReadLength();
Expand All @@ -55,14 +57,20 @@
var reader = Reader;

var length = _length!.Value;

while (true)
{
var result = await reader.ReadAsync();

if (result.Buffer.Length < length)
{
reader.AdvanceTo(result.Buffer.Start, result.Buffer.End);

if (result.IsCompleted)
{
throw new ProviderException(ResponseStatus.BadRequest, "Unexpected end of body");
}

continue;
}

Expand All @@ -71,7 +79,7 @@
if (buffer.IsSingleSegment)
{
_readResult = result;
return (_memory = buffer.First).Value;

Check warning on line 82 in Engine/Shared/Types/Body/MemoryConsumptionStrategy.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Extract the assignment of '_memory' from this expression.

Check warning on line 82 in Engine/Shared/Types/Body/MemoryConsumptionStrategy.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Extract the assignment of '_memory' from this expression.

Check warning on line 82 in Engine/Shared/Types/Body/MemoryConsumptionStrategy.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Extract the assignment of '_memory' from this expression.

Check warning on line 82 in Engine/Shared/Types/Body/MemoryConsumptionStrategy.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Extract the assignment of '_memory' from this expression.
}

var linearized = GC.AllocateUninitializedArray<byte>((int)length);
Expand All @@ -80,18 +88,18 @@

reader.AdvanceTo(result.Buffer.GetPosition(length));

return (_memory = linearized).Value;

Check warning on line 91 in Engine/Shared/Types/Body/MemoryConsumptionStrategy.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Extract the assignment of '_memory' from this expression.

Check warning on line 91 in Engine/Shared/Types/Body/MemoryConsumptionStrategy.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Extract the assignment of '_memory' from this expression.

Check warning on line 91 in Engine/Shared/Types/Body/MemoryConsumptionStrategy.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Extract the assignment of '_memory' from this expression.
}
}

private async ValueTask<ReadOnlyMemory<byte>> ReadChunked()
{
var chunkedStream = new ChunkedBodyStream(Reader);

var writer = new ArrayBufferWriter<byte>();

var pool = ArrayPool<byte>.Shared;

var buffer = pool.Rent(16 * 1024);

try
Expand All @@ -115,10 +123,10 @@
}

await chunkedStream.DrainAsync();

return (_memory = writer.WrittenMemory).Value;

Check warning on line 127 in Engine/Shared/Types/Body/MemoryConsumptionStrategy.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Extract the assignment of '_memory' from this expression.

Check warning on line 127 in Engine/Shared/Types/Body/MemoryConsumptionStrategy.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Extract the assignment of '_memory' from this expression.

Check warning on line 127 in Engine/Shared/Types/Body/MemoryConsumptionStrategy.cs

View workflow job for this annotation

GitHub Actions / Test & Coverage

Extract the assignment of '_memory' from this expression.
}

public ValueTask DrainAsync()
{
if (_readResult is not null)
Expand Down
6 changes: 5 additions & 1 deletion Modules/DirectoryBrowsing/Provider/ListingRouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ public ListingRouter(IResourceTree tree)

public async ValueTask<IResponse?> HandleAsync(IRequest request)
{
var (node, resource) = await Tree.FindAsync(request.Header.Target);
var target = request.Header.Target;

target.DenyPathTraversal();

var (node, resource) = await Tree.FindAsync(target);

if (resource is not null)
{
Expand Down
4 changes: 3 additions & 1 deletion Modules/Files/Multi/AbstractAssetsHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ protected AbstractAssetsHandler(List<ICompressionAlgorithm> algorithms, char sep
public async ValueTask<IResponse?> HandleAsync(IRequest request)
{
var target = request.Header.Target;

if (target.HasTrailingSlash)
{
return null;
}

target.DenyPathTraversal();

if (_preCompression.Enabled)
{
var handled = await TryGetPreCompressed(request);
Expand Down
2 changes: 2 additions & 0 deletions Modules/Files/Multi/Ioxide/IoxideFilesHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ public ValueTask PrepareAsync(IServer server)

var target = request.Header.Target;

target.DenyPathTraversal();

if (target.HasTrailingSlash)
{
return default; // a directory request, not a file (no directory index)
Expand Down
58 changes: 58 additions & 0 deletions Modules/IO/Extensions.Request.Security.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Runtime.CompilerServices;

using GenHTTP.Api.Content;
using GenHTTP.Api.Protocol;

namespace GenHTTP.Modules.IO;

public static class RequestSecurityExtensions
{

/// <summary>
/// Checks all remaining, non-routed segments of the request target for
/// path traversal attacks and throws a provider exception if one
/// is detected.
/// </summary>
/// <param name="target">The request target to be checked</param>
public static void DenyPathTraversal(this IRequestTarget target)
{
var index = 0;

PathSegment? segment;

while ((segment = target.Next(index++)) != null)
{
if (IsDotSegment(segment.Value.Bytes.Span))
{
throw new ProviderException(ResponseStatus.BadRequest, "Potential path traversal detected");
}
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsDotSegment(ReadOnlySpan<byte> segment)
{
var dots = 0;

for (var i = 0; i < segment.Length;)
{
if (segment[i] == (byte)'.')
{
dots++;
i++;
}
else if (segment[i] == (byte)'%' && i + 2 < segment.Length && segment[i + 1] == (byte)'2' && (segment[i + 2] == (byte)'e' || segment[i + 2] == (byte)'E'))
{
dots++;
i += 3;
}
else
{
return false;
}
}

return dots is 1 or 2;
}

}
26 changes: 7 additions & 19 deletions Modules/IO/Ranges/RangedStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,31 +52,19 @@ public RangedStream(Stream target, ulong start, ulong end)

public override void Write(byte[] buffer, int offset, int count)
{
if (Position > End)
if (Position > End || (Position + count) <= Start)
{
Position += count;
return;
}

long actualOffset = offset;
long actualCount = count;
var writeStart = Math.Max(Position, Start);
var writeEnd = Math.Min(Position + count - 1, End);

if (Position < Start)
{
actualOffset += (int)(Start - Position);
actualCount -= (int)(Start - Position);
}

if ((Start + actualCount) > (End + 1))
{
actualCount = Math.Min(End - Start + 1, actualCount);
}

if (actualOffset < buffer.Length)
{
var toWrite = Math.Min(buffer.Length - actualOffset, actualCount);
var actualOffset = offset + (writeStart - Position);
var actualCount = writeEnd - writeStart + 1;

Target.Write(buffer, (int)actualOffset, (int)toWrite);
}
Target.Write(buffer, (int)actualOffset, (int)actualCount);

Position += count;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ public SinglePageProvider(IResourceTree tree, bool serverSideRouting)

public async ValueTask<IResponse?> HandleAsync(IRequest request)
{
if (request.Header.Target.Current == null)
var target = request.Header.Target;

if (target.Current == null)
{
var index = await GetIndex();

Expand All @@ -55,6 +57,8 @@ public SinglePageProvider(IResourceTree tree, bool serverSideRouting)
}
else
{
target.DenyPathTraversal();

var result = await Resources.HandleAsync(request);

if (result == null)
Expand Down
2 changes: 2 additions & 0 deletions Modules/StaticWebsites/Provider/StaticWebsiteHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public StaticWebsiteHandler(IResourceTree tree)
public async ValueTask<IResponse?> HandleAsync(IRequest request)
{
var target = request.Header.Target;

target.DenyPathTraversal();

if (target.HasTrailingSlash)
{
Expand Down
4 changes: 2 additions & 2 deletions Testing/Acceptance/Engine/Body/ChunkedBodyStreamTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.IO.Pipelines;
using System.Text;

using GenHTTP.Api.Content;
using GenHTTP.Engine.Shared.Types.Body;

namespace GenHTTP.Testing.Acceptance.Engine.Body;
Expand Down Expand Up @@ -87,7 +87,7 @@ public async Task TestTruncatedChunkThrows()
var stream = await CreateAsync("5\r\nHel");

#pragma warning disable CA2022 // expected to throw before any byte count is returned
await Assert.ThrowsExactlyAsync<InvalidDataException>(async () => await stream.ReadAsync(new byte[16]));
await Assert.ThrowsExactlyAsync<ProviderException>(async () => await stream.ReadAsync(new byte[16]));
#pragma warning restore CA2022
}

Expand Down
1 change: 0 additions & 1 deletion Testing/Acceptance/Engine/Body/DrainBodyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using GenHTTP.Api.Content;
using GenHTTP.Api.Infrastructure;
using GenHTTP.Api.Protocol;
using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine;

namespace GenHTTP.Testing.Acceptance.Engine.Body;

Expand Down
Loading
Loading