Skip to content
Closed
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
65 changes: 65 additions & 0 deletions Adapters/WiredIO/Adapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using GenHTTP.Adapters.WiredIO.Mapping;
using GenHTTP.Api.Content;
using GenHTTP.Api.Infrastructure;

using GenHTTP.Modules.ClientCaching;
using GenHTTP.Modules.Compression;
using GenHTTP.Modules.ErrorHandling;
using GenHTTP.Modules.IO;

using Wired.IO.Builder;
using Wired.IO.Http11Express;
using Wired.IO.Http11Express.Context;

namespace GenHTTP.Adapters.WiredIO;

public static class Adapter
{

// ToDo: IBaseRequest and IBaseResponse do not feature basic access (such as headers), so we cannot be generic here

public static void Map(this Builder<WiredHttp11Express<Http11ExpressContext>, Http11ExpressContext> builder, string path, IHandlerBuilder handler, IServerCompanion? companion = null)
=> Map(builder, path, handler.Build(), companion);

public static void Map(this Builder<WiredHttp11Express<Http11ExpressContext>, Http11ExpressContext> builder, string path, IHandler handler, IServerCompanion? companion = null)
{
builder.UseMiddleware(scope => async (c, n) => await Bridge.MapAsync(c, n, handler, companion: companion, registeredPath: path));
}

/// <summary>
/// Enables default features on the given handler. This should be used on the
/// outer-most handler only.
/// </summary>
/// <param name="builder">The handler to be configured</param>
/// <param name="errorHandling">If enabled, any exception will be catched and converted into an error response</param>
/// <param name="compression">If enabled, responses will automatically be compressed if possible</param>
/// <param name="clientCaching">If enabled, ETags are attached to any generated response and the tag is evaluated on the next request of the same resource</param>
/// <param name="rangeSupport">If enabled, clients can request ranges instead of the complete response body</param>
/// <typeparam name="T">The type of the handler builder which will be returned to allow the factory pattern</typeparam>
/// <returns>The handler builder instance to be chained</returns>
public static T Defaults<T>(this T builder, bool errorHandling = true, bool compression = true, bool clientCaching = true, bool rangeSupport = false) where T : IHandlerBuilder<T>
{
if (compression)
{
builder.Add(CompressedContent.Default());
}

if (rangeSupport)
{
builder.Add(RangeSupport.Create());
}

if (clientCaching)
{
builder.Add(ClientCache.Validation());
}

if (errorHandling)
{
builder.Add(ErrorHandler.Default());
}

return builder;
}

}
30 changes: 30 additions & 0 deletions Adapters/WiredIO/GenHTTP.Adapters.WiredIO.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>

<TargetFrameworks>net9.0</TargetFrameworks>

<Description>Adapter to run GenHTTP handlers within an Wired.IO app.</Description>
<PackageTags>Wired.IO Adapter GenHTTP</PackageTags>

</PropertyGroup>

<ItemGroup>

<PackageReference Include="Wired.IO" Version="9.5.3" />

<ProjectReference Include="..\..\API\GenHTTP.Api.csproj"/>

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

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

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

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

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

</ItemGroup>

</Project>
110 changes: 110 additions & 0 deletions Adapters/WiredIO/Mapping/Bridge.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using GenHTTP.Adapters.WiredIO.Server;
using GenHTTP.Adapters.WiredIO.Types;

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

using Wired.IO.Http11Express.Context;

using WR = Wired.IO.Protocol.Response;

namespace GenHTTP.Adapters.WiredIO.Mapping;

public static class Bridge
{

public static async Task MapAsync(Http11ExpressContext context, Func<Http11ExpressContext, Task> next, IHandler handler, IServerCompanion? companion = null, string? registeredPath = null)
{
if ((registeredPath != null) && !context.Request.Route.StartsWith(registeredPath))
{
await next(context);
return;
}

// todo: can we cache this somewhere?
var server = new ImplicitServer(handler, companion);

try
{
using var request = new Request(server, context.Request);

if (registeredPath != null)
{
AdvanceTo(request, registeredPath);
}

using var response = await handler.HandleAsync(request);

if (response != null)
{
MapResponse(response, context);

server.Companion?.OnRequestHandled(request, response);
}
else
{
await next(context);
}
}
catch (Exception e)
{
// todo: cannot tell the IP of the client in wired
server.Companion?.OnServerError(ServerErrorScope.ServerConnection, null, e);
throw;
}
}

private static void MapResponse(IResponse response, Http11ExpressContext context)
{
var target = context.Respond();

target.Status((WR.ResponseStatus)response.Status.RawStatus);

foreach (var header in response.Headers)
{
target.Header(header.Key, header.Value);
}

if (response.Modified != null)
{
target.Header("Last-Modified", response.Modified.Value.ToUniversalTime().ToString("r"));
}

if (response.Expires != null)
{
target.Header("Expires", response.Expires.Value.ToUniversalTime().ToString("r"));
}

if (response.HasCookies)
{
foreach (var cookie in response.Cookies)
{
target.Header("Set-Cookie", $"{cookie.Key}={cookie.Value.Value}");
}
}

if (response.Content != null)
{
target.Content(new MappedContent(response));

target.Header("Content-Type", (response.ContentType?.Charset != null ? $"{response.ContentType?.RawType}; charset={response.ContentType?.Charset}" : response.ContentType?.RawType) ?? "application/octet-stream");

if (response.ContentEncoding != null)
{
target.Header("Content-Encoding", response.ContentEncoding);
}
}
}

private static void AdvanceTo(Request request, string registeredPath)
{
var parts = registeredPath.Split('/', StringSplitOptions.RemoveEmptyEntries);

foreach (var _ in parts)
{
request.Target.Advance();
}
}

}
5 changes: 5 additions & 0 deletions Adapters/WiredIO/Server/EmptyEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using GenHTTP.Api.Infrastructure;

namespace GenHTTP.Adapters.WiredIO.Server;

public class EmptyEndpoints : List<IEndPoint>, IEndPointCollection;
56 changes: 56 additions & 0 deletions Adapters/WiredIO/Server/ImplicitServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Runtime.InteropServices;
using GenHTTP.Api.Content;
using GenHTTP.Api.Infrastructure;

namespace GenHTTP.Adapters.WiredIO.Server;

public sealed class ImplicitServer : IServer
{

#region Get-/Setters

public string Version => RuntimeInformation.FrameworkDescription;

public bool Running { get; }

public bool Development
{
get
{
// todo: is there something like development mode in wired?
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
return string.Compare(env, "Development", StringComparison.OrdinalIgnoreCase) == 0;
}
}

public IEndPointCollection EndPoints { get; }

public IServerCompanion? Companion { get; }

public IHandler Handler { get; }

#endregion

#region Initialization

public ImplicitServer(IHandler handler, IServerCompanion? companion)
{
Handler = handler;
Companion = companion;

EndPoints = new EmptyEndpoints();

Running = true;
}

#endregion

#region Functionality

public ValueTask DisposeAsync() => new();

public ValueTask StartAsync() => throw new InvalidOperationException("Server is managed by WiredIO and cannot be started");

#endregion

}
40 changes: 40 additions & 0 deletions Adapters/WiredIO/Types/ClientConnection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Net;
using System.Security.Cryptography.X509Certificates;

using GenHTTP.Api.Infrastructure;
using GenHTTP.Api.Protocol;

using Wired.IO.Http11Express.Request;

namespace GenHTTP.Adapters.WiredIO.Types;

public sealed class ClientConnection : IClientConnection
{

#region Get-/Setters

public IPAddress IPAddress => throw new InvalidOperationException("Remote client IP address is not known");

public ClientProtocol? Protocol { get; }

public string? Host => Request.Headers.GetValueOrDefault("Host");

public X509Certificate? Certificate => null;

private IExpressRequest Request { get; }

#endregion

#region Initialization

public ClientConnection(IExpressRequest request)
{
Request = request;

// todo: wired does not expose this information
Protocol = ClientProtocol.Http;
}

#endregion

}
81 changes: 81 additions & 0 deletions Adapters/WiredIO/Types/Headers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System.Collections;

using GenHTTP.Api.Protocol;

using Wired.IO.Http11Express.Request;

namespace GenHTTP.Adapters.WiredIO.Types;

public sealed class Headers : IHeaderCollection
{

#region Get-/Setters

public int Count => Request.Headers.Count;

public bool ContainsKey(string key) => Request.Headers.ContainsKey(key);

public bool TryGetValue(string key, out string value)
{
if (Request.Headers.TryGetValue(key, out var found))
{
value = found;
return true;
}

value = string.Empty;
return false;
}

public string this[string key] => ContainsKey(key) ? Request.Headers[key] : string.Empty;

public IEnumerable<string> Keys => Request.Headers.Keys;

public IEnumerable<string> Values
{
get
{
foreach (var entry in Request.Headers)
{
yield return entry.Value;
}
}
}

private IExpressRequest Request { get; }

#endregion

#region Initialization

public Headers(IExpressRequest request)
{
Request = request;
}

#endregion

#region Functionality

public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
{
foreach (var entry in Request.Headers)
{
yield return new(entry.Key, entry.Value);
}
}

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

#endregion

#region Lifecycle

public void Dispose()
{

}

#endregion

}
Loading
Loading