From 4daa5a38bc8a1dcdb455c138693f0d2a04aac1b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20N=C3=A4geli?= Date: Fri, 17 Oct 2025 17:16:37 +0200 Subject: [PATCH 1/5] Add Wired.IO as an engine --- Adapters/WiredIO/Adapter.cs | 74 +++++++++ .../WiredIO/GenHTTP.Adapters.WiredIO.csproj | 30 ++++ Adapters/WiredIO/Mapping/Bridge.cs | 110 +++++++++++++ Adapters/WiredIO/Server/EmptyEndpoints.cs | 5 + Adapters/WiredIO/Server/ImplicitServer.cs | 55 +++++++ Adapters/WiredIO/Types/ClientConnection.cs | 40 +++++ Adapters/WiredIO/Types/Cookies.cs | 21 +++ Adapters/WiredIO/Types/Headers.cs | 87 ++++++++++ Adapters/WiredIO/Types/Query.cs | 87 ++++++++++ Adapters/WiredIO/Types/Request.cs | 141 ++++++++++++++++ Engine/WiredIO/GenHTTP.Engine.WiredIO.csproj | 28 ++++ Engine/WiredIO/Host.cs | 24 +++ Engine/WiredIO/Hosting/WiredEndpoint.cs | 36 +++++ Engine/WiredIO/Hosting/WiredEndpoints.cs | 5 + Engine/WiredIO/Hosting/WiredServer.cs | 151 ++++++++++++++++++ Engine/WiredIO/Hosting/WiredServerBuilder.cs | 27 ++++ Engine/WiredIO/Server.cs | 28 ++++ GenHTTP.slnx | 2 + 18 files changed, 951 insertions(+) create mode 100644 Adapters/WiredIO/Adapter.cs create mode 100644 Adapters/WiredIO/GenHTTP.Adapters.WiredIO.csproj create mode 100644 Adapters/WiredIO/Mapping/Bridge.cs create mode 100644 Adapters/WiredIO/Server/EmptyEndpoints.cs create mode 100644 Adapters/WiredIO/Server/ImplicitServer.cs create mode 100644 Adapters/WiredIO/Types/ClientConnection.cs create mode 100644 Adapters/WiredIO/Types/Cookies.cs create mode 100644 Adapters/WiredIO/Types/Headers.cs create mode 100644 Adapters/WiredIO/Types/Query.cs create mode 100644 Adapters/WiredIO/Types/Request.cs create mode 100644 Engine/WiredIO/GenHTTP.Engine.WiredIO.csproj create mode 100644 Engine/WiredIO/Host.cs create mode 100644 Engine/WiredIO/Hosting/WiredEndpoint.cs create mode 100644 Engine/WiredIO/Hosting/WiredEndpoints.cs create mode 100644 Engine/WiredIO/Hosting/WiredServer.cs create mode 100644 Engine/WiredIO/Hosting/WiredServerBuilder.cs create mode 100644 Engine/WiredIO/Server.cs diff --git a/Adapters/WiredIO/Adapter.cs b/Adapters/WiredIO/Adapter.cs new file mode 100644 index 000000000..53e142f3f --- /dev/null +++ b/Adapters/WiredIO/Adapter.cs @@ -0,0 +1,74 @@ +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.App; +using Wired.IO.Http11.Context; + +namespace GenHTTP.Adapters.WiredIO; + +public static class Adapter +{ + + /// + /// Registers the given handler to respond to requests to the specified path. + /// + /// The application to add the mapping to + /// The path to register the handler for + /// The handler to be registered + /// An object that will be informed about handled requests and any error + public static void Map(this WiredApp app, string path, IHandlerBuilder handler, IServerCompanion? companion = null) + => Map(app, path, handler.Build(), companion); + + /// + /// Registers the given handler to respond to requests to the specified path. + /// + /// The application to add the mapping to + /// The path to register the handler for + /// The handler to be registered + /// An object that will be informed about handled requests and any error + public static void Map(this WiredApp app, string path, IHandler handler, IServerCompanion? companion = null) + => app.Map(path + "/{*any}", async context => await Bridge.MapAsync(context, handler, companion: companion, registeredPath: path)); + + /// + /// Enables default features on the given handler. This should be used on the + /// outer-most handler only. + /// + /// The handler to be configured + /// If enabled, any exception will be catched and converted into an error response + /// If enabled, responses will automatically be compressed if possible + /// If enabled, ETags are attached to any generated response and the tag is evaluated on the next request of the same resource + /// If enabled, clients can request ranges instead of the complete response body + /// The type of the handler builder which will be returned to allow the factory pattern + /// The handler builder instance to be chained + public static T Defaults(this T builder, bool errorHandling = true, bool compression = true, bool clientCaching = true, bool rangeSupport = false) where T : IHandlerBuilder + { + 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; + } + +} diff --git a/Adapters/WiredIO/GenHTTP.Adapters.WiredIO.csproj b/Adapters/WiredIO/GenHTTP.Adapters.WiredIO.csproj new file mode 100644 index 000000000..cfe4eeebb --- /dev/null +++ b/Adapters/WiredIO/GenHTTP.Adapters.WiredIO.csproj @@ -0,0 +1,30 @@ + + + + + net9.0 + + Adapter to run GenHTTP handlers within an Wired.IO app. + Wired.IO Adapter GenHTTP + + + + + + + + + + + + + + + + + + + + + + diff --git a/Adapters/WiredIO/Mapping/Bridge.cs b/Adapters/WiredIO/Mapping/Bridge.cs new file mode 100644 index 000000000..1686c0d22 --- /dev/null +++ b/Adapters/WiredIO/Mapping/Bridge.cs @@ -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; + +namespace GenHTTP.Adapters.WiredIO.Mapping; + +public static class Bridge +{ + + public static async ValueTask MapAsync(HttpContext context, IHandler handler, IServer? server = null, IServerCompanion? companion = null, string? registeredPath = null) + { + var actualServer = server ?? new ImplicitServer(handler, companion); + + try + { + using var request = new Request(actualServer, context); + + if (registeredPath != null) + { + AdvanceTo(request, registeredPath); + } + + using var response = await handler.HandleAsync(request); + + if (response == null) + { + context.Response.StatusCode = 404; + } + else + { + await WriteAsync(response, context); + + actualServer.Companion?.OnRequestHandled(request, response); + } + } + catch (Exception e) + { + actualServer.Companion?.OnServerError(ServerErrorScope.ServerConnection, context.Connection.RemoteIpAddress, e); + throw; + } + } + + private static async ValueTask WriteAsync(IResponse response, HttpContext context) + { + var target = context.Response; + + target.StatusCode = response.Status.RawStatus; + + foreach (var header in response.Headers) + { + target.Headers.Append(header.Key, header.Value); + } + + if (response.Modified != null) + { + target.Headers.LastModified = response.Modified.Value.ToUniversalTime().ToString("r"); + } + + if (response.Expires != null) + { + target.Headers.Expires = response.Expires.Value.ToUniversalTime().ToString("r"); + } + + if (response.HasCookies) + { + foreach (var cookie in response.Cookies) + { + if (cookie.Value.MaxAge != null) + { + target.Cookies.Append(cookie.Key, cookie.Value.Value, new() + { + MaxAge = TimeSpan.FromSeconds(cookie.Value.MaxAge.Value) + }); + } + else + { + target.Cookies.Append(cookie.Key, cookie.Value.Value); + } + } + } + + if (response.Content != null) + { + target.ContentLength = (long?)response.ContentLength ?? (long?)response.Content.Length; + + target.ContentType = response.ContentType?.Charset != null ? $"{response.ContentType?.RawType}; charset={response.ContentType?.Charset}" : response.ContentType?.RawType; + + if (response.ContentEncoding != null) + { + target.Headers.ContentEncoding = response.ContentEncoding; + } + + await response.Content.WriteAsync(target.Body, 65 * 1024); + } + } + + private static void AdvanceTo(Request request, string registeredPath) + { + var parts = registeredPath.Split('/', StringSplitOptions.RemoveEmptyEntries); + + foreach (var _ in parts) + { + request.Target.Advance(); + } + } + +} diff --git a/Adapters/WiredIO/Server/EmptyEndpoints.cs b/Adapters/WiredIO/Server/EmptyEndpoints.cs new file mode 100644 index 000000000..27adb9735 --- /dev/null +++ b/Adapters/WiredIO/Server/EmptyEndpoints.cs @@ -0,0 +1,5 @@ +using GenHTTP.Api.Infrastructure; + +namespace GenHTTP.Adapters.WiredIO.Server; + +public class EmptyEndpoints : List, IEndPointCollection; diff --git a/Adapters/WiredIO/Server/ImplicitServer.cs b/Adapters/WiredIO/Server/ImplicitServer.cs new file mode 100644 index 000000000..77dffdd5e --- /dev/null +++ b/Adapters/WiredIO/Server/ImplicitServer.cs @@ -0,0 +1,55 @@ +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 + { + 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 ASP.NET Core and cannot be started"); + + #endregion + +} diff --git a/Adapters/WiredIO/Types/ClientConnection.cs b/Adapters/WiredIO/Types/ClientConnection.cs new file mode 100644 index 000000000..f84526a67 --- /dev/null +++ b/Adapters/WiredIO/Types/ClientConnection.cs @@ -0,0 +1,40 @@ +using System.Net; +using System.Security.Cryptography.X509Certificates; + +using GenHTTP.Api.Infrastructure; +using GenHTTP.Api.Protocol; + +namespace GenHTTP.Adapters.WiredIO.Types; + +public sealed class ClientConnection : IClientConnection +{ + + #region Get-/Setters + + public IPAddress IPAddress => Info.RemoteIpAddress ?? throw new InvalidOperationException("Remote client IP address is not known"); + + public ClientProtocol? Protocol { get; } + + public string? Host => Request.Host.HasValue ? Request.Host.Value : null; + + public X509Certificate? Certificate => Info.ClientCertificate; + + private ConnectionInfo Info { get; } + + private HttpRequest Request { get; } + + #endregion + + #region Initialization + + public ClientConnection(ConnectionInfo info, HttpRequest request) + { + Info = info; + Request = request; + + Protocol = (request.IsHttps) ? ClientProtocol.Https : ClientProtocol.Http; + } + + #endregion + +} diff --git a/Adapters/WiredIO/Types/Cookies.cs b/Adapters/WiredIO/Types/Cookies.cs new file mode 100644 index 000000000..be8be6613 --- /dev/null +++ b/Adapters/WiredIO/Types/Cookies.cs @@ -0,0 +1,21 @@ +using GenHTTP.Api.Protocol; + +namespace GenHTTP.Adapters.WiredIO.Types; + +public sealed class Cookies : Dictionary, ICookieCollection +{ + + public Cookies(HttpRequest request) + { + foreach (var cookie in request.Cookies) + { + Add(cookie.Key, new(cookie.Key, cookie.Value)); + } + } + + public void Dispose() + { + + } + +} diff --git a/Adapters/WiredIO/Types/Headers.cs b/Adapters/WiredIO/Types/Headers.cs new file mode 100644 index 000000000..2aa82f8db --- /dev/null +++ b/Adapters/WiredIO/Types/Headers.cs @@ -0,0 +1,87 @@ +using System.Collections; +using GenHTTP.Api.Protocol; + +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 strings)) + { + value = strings.FirstOrDefault() ?? string.Empty; + return true; + } + + value = string.Empty; + return false; + } + + public string this[string key] => Request.Headers[key].FirstOrDefault() ?? string.Empty; + + public IEnumerable Keys => Request.Headers.Keys; + + public IEnumerable Values + { + get + { + foreach (var entry in Request.Headers) + { + foreach (var value in entry.Value) + { + if (value != null) yield return value; + } + } + } + } + + private HttpRequest Request { get; } + + #endregion + + #region Initialization + + public Headers(HttpRequest request) + { + Request = request; + } + + #endregion + + #region Functionality + + public IEnumerator> GetEnumerator() + { + foreach (var entry in Request.Headers) + { + foreach (var stringEntry in entry.Value) + { + if (stringEntry != null) + { + yield return new(entry.Key, stringEntry); + } + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + #endregion + + #region Lifecycle + + public void Dispose() + { + + } + + #endregion + +} diff --git a/Adapters/WiredIO/Types/Query.cs b/Adapters/WiredIO/Types/Query.cs new file mode 100644 index 000000000..46e0ea2d3 --- /dev/null +++ b/Adapters/WiredIO/Types/Query.cs @@ -0,0 +1,87 @@ +using System.Collections; +using GenHTTP.Api.Protocol; + +namespace GenHTTP.Adapters.WiredIO.Types; + +public sealed class Query : IRequestQuery +{ + + #region Get-/Setters + + public int Count => Request.Query.Count; + + public bool ContainsKey(string key) => Request.Query.ContainsKey(key); + + public bool TryGetValue(string key, out string value) + { + if (Request.Query.TryGetValue(key, out var stringValue)) + { + value = stringValue.FirstOrDefault() ?? string.Empty; + return true; + } + + value = string.Empty; + return false; + } + + public string this[string key] => Request.Query[key][0] ?? string.Empty; + + public IEnumerable Keys => Request.Query.Keys; + + public IEnumerable Values + { + get + { + foreach (var entry in Request.Query) + { + foreach (var value in entry.Value) + { + if (value != null) yield return value; + } + } + } + } + + private HttpRequest Request { get; } + + #endregion + + #region Initialization + + public Query(HttpRequest request) + { + Request = request; + } + + #endregion + + #region Functionality + + public IEnumerator> GetEnumerator() + { + foreach (var entry in Request.Query) + { + foreach (var stringEntry in entry.Value) + { + if (stringEntry != null) + { + yield return new(entry.Key, stringEntry); + } + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + #endregion + + #region Lifecycle + + public void Dispose() + { + + } + + #endregion + +} diff --git a/Adapters/WiredIO/Types/Request.cs b/Adapters/WiredIO/Types/Request.cs new file mode 100644 index 000000000..082d9ba16 --- /dev/null +++ b/Adapters/WiredIO/Types/Request.cs @@ -0,0 +1,141 @@ +using GenHTTP.Api.Infrastructure; +using GenHTTP.Api.Protocol; +using GenHTTP.Api.Routing; +using GenHTTP.Engine.Shared.Types; +using HttpProtocol = GenHTTP.Api.Protocol.HttpProtocol; + +namespace GenHTTP.Adapters.WiredIO.Types; + +public sealed class Request : IRequest +{ + private RequestProperties? _Properties; + + private Query? _Query; + + private Cookies? _Cookies; + + private readonly ForwardingCollection _Forwardings = new(); + + private Headers? _Headers; + + private readonly IEndPoint? _EndPoint; + + #region Get-/Setters + + public IRequestProperties Properties + { + get { return _Properties ??= new RequestProperties(); } + } + + public IServer Server { get; } + + public IEndPoint EndPoint => _EndPoint ?? throw new InvalidOperationException("EndPoint is not available as it is managed by ASP.NET Core"); + + public IClientConnection Client { get; } + + public IClientConnection LocalClient { get; } + + public HttpProtocol ProtocolType { get; } + + public FlexibleRequestMethod Method { get; } + + public RoutingTarget Target { get; } + + public string? UserAgent => this["User-Agent"]; + + public string? Referer => this["Referer"]; + + public string? Host => this["Host"]; + + public string? this[string additionalHeader] => Headers.GetValueOrDefault(additionalHeader); + + public IRequestQuery Query + { + get { return _Query ??= new Query(Context); } + } + + public ICookieCollection Cookies + { + get { return _Cookies ??= new Cookies(Context); } + } + + public IForwardingCollection Forwardings => _Forwardings; + + public IHeaderCollection Headers + { + get { return _Headers ??= new Headers(Context); } + } + + public Stream Content => Context.BodyReader.AsStream(true); + + public FlexibleContentType? ContentType => (Context.ContentType != null) ? new(Context.ContentType) : null; + + private HttpRequest Context { get; } + + #endregion + + #region Initialization + + public Request(IServer server, HttpContext context) + { + Server = server; + Context = context.Request; + + ProtocolType = Context.Protocol switch + { + "HTTP/1.0" => HttpProtocol.Http10, + "HTTP/1.1" => HttpProtocol.Http11, + "HTTP/2" => HttpProtocol.Http2, + "HTTP/3" => HttpProtocol.Http3, + _ => HttpProtocol.Http11 + }; + + Method = FlexibleRequestMethod.Get(Context.Method); + Target = new RoutingTarget(WebPath.FromString(Context.Path)); + + if (context.Request.Headers.TryGetValue("forwarded", out var forwardings)) + { + foreach (var entry in forwardings) + { + if (entry != null) _Forwardings.Add(entry); + } + } + else + { + _Forwardings.TryAddLegacy(Headers); + } + + LocalClient = new ClientConnection(context.Connection, context.Request); + + Client = _Forwardings.DetermineClient(context.Connection.ClientCertificate) ?? LocalClient; + + _EndPoint = Server.EndPoints.FirstOrDefault(e => e.Port == context.Connection.LocalPort); + } + + #endregion + + #region Functionality + + public IResponseBuilder Respond() => new ResponseBuilder().Status(ResponseStatus.Ok); + + public UpgradeInfo Upgrade() => throw new NotSupportedException("Web sockets are not supported by the Kestrel server implementation"); + + #endregion + + #region Lifecycle + + private bool _Disposed; + + public void Dispose() + { + if (!_Disposed) + { + _Properties?.Dispose(); + + _Disposed = true; + } + } + + #endregion + +} diff --git a/Engine/WiredIO/GenHTTP.Engine.WiredIO.csproj b/Engine/WiredIO/GenHTTP.Engine.WiredIO.csproj new file mode 100644 index 000000000..c5004f1b2 --- /dev/null +++ b/Engine/WiredIO/GenHTTP.Engine.WiredIO.csproj @@ -0,0 +1,28 @@ + + + + + net9.0 + + Web server engine based on Wired.IO that can be used to develop applications using the GenHTTP development framework + HTTP Embedded Webserver Server Library C# Wired.IO Engine + + + + + + + + + + + + + + + + + + + + diff --git a/Engine/WiredIO/Host.cs b/Engine/WiredIO/Host.cs new file mode 100644 index 000000000..91b50b748 --- /dev/null +++ b/Engine/WiredIO/Host.cs @@ -0,0 +1,24 @@ +using GenHTTP.Api.Infrastructure; + +using GenHTTP.Engine.Shared.Hosting; +using Wired.IO.App; +using Wired.IO.Builder; +using Wired.IO.Http11; +using Wired.IO.Http11.Context; + +namespace GenHTTP.Engine.WiredIO; + +public static class Host +{ + + /// + /// Provides a new server host that can be used to run a + /// server instance of the GenHTTP webserver. + /// + /// An action invoked with the pre-configured web application builder, allowing to customize the underlying ASP.NET app + /// An action invoked with the created application instance, allowing to customize the underlying ASP.NET app + /// The host which can be used to run a server instance + public static IServerHost Create(Action>? configurationHook = null, Action>? applicationHook = null) + => new ServerHost(Server.Create(configurationHook, applicationHook)); + +} diff --git a/Engine/WiredIO/Hosting/WiredEndpoint.cs b/Engine/WiredIO/Hosting/WiredEndpoint.cs new file mode 100644 index 000000000..55d4520ed --- /dev/null +++ b/Engine/WiredIO/Hosting/WiredEndpoint.cs @@ -0,0 +1,36 @@ +using System.Net; +using GenHTTP.Api.Infrastructure; + +namespace GenHTTP.Engine.WiredIO.Hosting; + +public sealed class WiredEndpoint : IEndPoint +{ + + #region Get-/Setters + + public IPAddress? Address { get; } + + public ushort Port { get; } + + public bool Secure { get; } + + #endregion + + #region Initialization + + public WiredEndpoint(IPAddress? address, ushort port, bool secure) + { + Address = address; + Port = port; + Secure = secure; + } + + #endregion + + #region Lifecycle + + public void Dispose() { } + + #endregion + +} diff --git a/Engine/WiredIO/Hosting/WiredEndpoints.cs b/Engine/WiredIO/Hosting/WiredEndpoints.cs new file mode 100644 index 000000000..cab44a504 --- /dev/null +++ b/Engine/WiredIO/Hosting/WiredEndpoints.cs @@ -0,0 +1,5 @@ +using GenHTTP.Api.Infrastructure; + +namespace GenHTTP.Engine.WiredIO.Hosting; + +public sealed class WiredEndpoints : List, IEndPointCollection; diff --git a/Engine/WiredIO/Hosting/WiredServer.cs b/Engine/WiredIO/Hosting/WiredServer.cs new file mode 100644 index 000000000..0540a12ff --- /dev/null +++ b/Engine/WiredIO/Hosting/WiredServer.cs @@ -0,0 +1,151 @@ +using System.Net; +using System.Net.Security; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Infrastructure; +using GenHTTP.Engine.Shared.Infrastructure; + +using Wired.IO.App; +using Wired.IO.Builder; +using Wired.IO.Http11; +using Wired.IO.Http11.Context; + +namespace GenHTTP.Engine.WiredIO.Hosting; + +internal sealed class WiredServer : IServer +{ + + #region Get-/Setters + + public string Version { get; } + + public bool Running { get; private set; } + + public bool Development { get; } + + public IEndPointCollection EndPoints { get; } + + public IServerCompanion? Companion { get; } + + public IHandler Handler { get; } + + private ServerConfiguration Configuration { get; } + + private WiredApp Application { get; } + + #endregion + + #region Initialization + + internal WiredServer(IServerCompanion? companion, ServerConfiguration configuration, IHandler handler, Action>? configurationHook, Action>? applicationHook) + { + Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "(n/a)"; + + Companion = companion; + Configuration = configuration; + + Development = configuration.DevelopmentMode; + + Handler = handler; + + var endpoints = new WiredEndpoints(); + + endpoints.AddRange(configuration.EndPoints.Select(e => new WiredEndpoint(e.Address, e.Port, e.Security is not null))); + + EndPoints = endpoints; + + Application = Spawn(configurationHook, applicationHook); + } + + #endregion + + #region Functionality + + private WiredApp Spawn(Action>? configurationHook, Action>? applicationHook) + { + var builder = WiredApp.CreateBuilder(); + + Configure(builder); + + configurationHook?.Invoke(builder); + + var app = builder.Build(); + + app.Run(); // todo: adapter goes here (app.BuildPipeline) + + applicationHook?.Invoke(app); + + return app; + } + + public async ValueTask StartAsync() + { + await Handler.PrepareAsync(); + + await Application.StartAsync(); + + Running = true; + } + + private void Configure(Builder builder) + { + foreach (var endpoint in Configuration.EndPoints) + { + builder.Endpoint(endpoint.Address ?? IPAddress.Any, endpoint.Port); + } + + var security = Secure(Configuration.EndPoints); + + if (security != null) + { + builder.UseTls(security); + } + } + + private static SslServerAuthenticationOptions? Secure(IEnumerable endpoints) + { + foreach (var endpoint in endpoints) + { + // todo: wired does not allow security-by-endpoint + if (endpoint.Security != null) + { + var security = endpoint.Security; + var validator = security.CertificateValidator; + + return new SslServerAuthenticationOptions() + { + EnabledSslProtocols = security.Protocols, + ServerCertificateSelectionCallback = (_, hostName) => security.CertificateProvider.Provide(hostName)!, + ClientCertificateRequired = validator?.RequireCertificate ?? false, + CertificateRevocationCheckMode = validator?.RevocationCheck ?? X509RevocationMode.NoCheck, + RemoteCertificateValidationCallback = (_, certificate, chain, sslPolicyErrors) => validator == null || validator.Validate(certificate, chain, sslPolicyErrors) + }; + } + } + + return null; + } + + #endregion + + #region Lifecycle + + private bool _Disposed; + + public async ValueTask DisposeAsync() + { + if (!_Disposed) + { + await Application.StopAsync(); + + Application.Dispose(); + + _Disposed = true; + } + } + + #endregion + +} diff --git a/Engine/WiredIO/Hosting/WiredServerBuilder.cs b/Engine/WiredIO/Hosting/WiredServerBuilder.cs new file mode 100644 index 000000000..ddc812e76 --- /dev/null +++ b/Engine/WiredIO/Hosting/WiredServerBuilder.cs @@ -0,0 +1,27 @@ +using GenHTTP.Api.Content; +using GenHTTP.Api.Infrastructure; + +using GenHTTP.Engine.Shared.Infrastructure; + +using Wired.IO.App; +using Wired.IO.Builder; +using Wired.IO.Http11; +using Wired.IO.Http11.Context; + +namespace GenHTTP.Engine.WiredIO.Hosting; + +public sealed class WiredServerBuilder : ServerBuilder +{ + private readonly Action>? _ConfigurationHook; + + private readonly Action>? _ApplicationHook; + + public WiredServerBuilder(Action>? configurationHook, Action>? applicationHook) + { + _ConfigurationHook = configurationHook; + _ApplicationHook = applicationHook; + } + + protected override IServer Build(IServerCompanion? companion, ServerConfiguration config, IHandler handler) => new WiredServer(companion, config, handler, _ConfigurationHook, _ApplicationHook); + +} diff --git a/Engine/WiredIO/Server.cs b/Engine/WiredIO/Server.cs new file mode 100644 index 000000000..45e2aeeba --- /dev/null +++ b/Engine/WiredIO/Server.cs @@ -0,0 +1,28 @@ +using GenHTTP.Api.Infrastructure; + +using GenHTTP.Engine.WiredIO.Hosting; + +using Wired.IO.App; +using Wired.IO.Builder; +using Wired.IO.Http11; +using Wired.IO.Http11.Context; + +namespace GenHTTP.Engine.WiredIO; + +/// +/// Allows to create server instances. +/// +public static class Server +{ + + /// + /// Create a new, configurable server instance with + /// default values. + /// + /// An action invoked with the pre-configured web application builder, allowing to customize the underlying ASP.NET app + /// An action invoked with the created application instance, allowing to customize the underlying ASP.NET app + /// The builder to create the instance + public static IServerBuilder Create(Action>? configurationHook = null, Action>? applicationHook = null) + => new WiredServerBuilder(configurationHook, applicationHook); + +} diff --git a/GenHTTP.slnx b/GenHTTP.slnx index a1d3af5d7..1c38f7f1d 100644 --- a/GenHTTP.slnx +++ b/GenHTTP.slnx @@ -1,6 +1,7 @@  + @@ -9,6 +10,7 @@ + From e53f8f21293333e6f3f9d7528fc306f756920cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20N=C3=A4geli?= Date: Tue, 21 Oct 2025 17:36:11 +0200 Subject: [PATCH 2/5] Further progress --- Adapters/WiredIO/Adapter.cs | 6 ++++++ Adapters/WiredIO/Mapping/Bridge.cs | 4 +++- Adapters/WiredIO/Types/Query.cs | 5 +++-- Adapters/WiredIO/Types/Request.cs | 11 ++++++++--- Engine/WiredIO/Hosting/WiredServer.cs | 4 ++-- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Adapters/WiredIO/Adapter.cs b/Adapters/WiredIO/Adapter.cs index 53e142f3f..799795c24 100644 --- a/Adapters/WiredIO/Adapter.cs +++ b/Adapters/WiredIO/Adapter.cs @@ -35,6 +35,12 @@ public static void Map(this WiredApp app, string path, IHandlerBu public static void Map(this WiredApp app, string path, IHandler handler, IServerCompanion? companion = null) => app.Map(path + "/{*any}", async context => await Bridge.MapAsync(context, handler, companion: companion, registeredPath: path)); + public static void BuildPipeline(this WiredApp app, IHandlerBuilder handler, IServer server) + => app.BuildPipeline(handler, server); + + public static void BuildPipeline(this WiredApp app, IHandler handler, IServer server) + => app.BuildPipeline(new List, Task>>(), context => Bridge.MapAsync(context, handler, server)); + /// /// Enables default features on the given handler. This should be used on the /// outer-most handler only. diff --git a/Adapters/WiredIO/Mapping/Bridge.cs b/Adapters/WiredIO/Mapping/Bridge.cs index 1686c0d22..bce16a6a9 100644 --- a/Adapters/WiredIO/Mapping/Bridge.cs +++ b/Adapters/WiredIO/Mapping/Bridge.cs @@ -5,12 +5,14 @@ using GenHTTP.Api.Infrastructure; using GenHTTP.Api.Protocol; +using Wired.IO.Http11.Context; + namespace GenHTTP.Adapters.WiredIO.Mapping; public static class Bridge { - public static async ValueTask MapAsync(HttpContext context, IHandler handler, IServer? server = null, IServerCompanion? companion = null, string? registeredPath = null) + public static async ValueTask MapAsync(Http11Context context, IHandler handler, IServer? server = null, IServerCompanion? companion = null, string? registeredPath = null) { var actualServer = server ?? new ImplicitServer(handler, companion); diff --git a/Adapters/WiredIO/Types/Query.cs b/Adapters/WiredIO/Types/Query.cs index 46e0ea2d3..f189d9160 100644 --- a/Adapters/WiredIO/Types/Query.cs +++ b/Adapters/WiredIO/Types/Query.cs @@ -1,5 +1,6 @@ using System.Collections; using GenHTTP.Api.Protocol; +using Wired.IO.Http11.Context; namespace GenHTTP.Adapters.WiredIO.Types; @@ -42,13 +43,13 @@ public IEnumerable Values } } - private HttpRequest Request { get; } + private Http11Context Request { get; } #endregion #region Initialization - public Query(HttpRequest request) + public Query(Http11Context request) { Request = request; } diff --git a/Adapters/WiredIO/Types/Request.cs b/Adapters/WiredIO/Types/Request.cs index 082d9ba16..708c78a54 100644 --- a/Adapters/WiredIO/Types/Request.cs +++ b/Adapters/WiredIO/Types/Request.cs @@ -2,8 +2,13 @@ using GenHTTP.Api.Protocol; using GenHTTP.Api.Routing; using GenHTTP.Engine.Shared.Types; + +using Wired.IO.Http11.Context; + using HttpProtocol = GenHTTP.Api.Protocol.HttpProtocol; +using WiredRequest = Wired.IO.Http11.Request.IRequest; + namespace GenHTTP.Adapters.WiredIO.Types; public sealed class Request : IRequest @@ -70,18 +75,18 @@ public IHeaderCollection Headers public FlexibleContentType? ContentType => (Context.ContentType != null) ? new(Context.ContentType) : null; - private HttpRequest Context { get; } + private WiredRequest Context { get; } #endregion #region Initialization - public Request(IServer server, HttpContext context) + public Request(IServer server, Http11Context context) { Server = server; Context = context.Request; - ProtocolType = Context.Protocol switch + ProtocolType = Context.ConnectionType switch { "HTTP/1.0" => HttpProtocol.Http10, "HTTP/1.1" => HttpProtocol.Http11, diff --git a/Engine/WiredIO/Hosting/WiredServer.cs b/Engine/WiredIO/Hosting/WiredServer.cs index 0540a12ff..3014915fc 100644 --- a/Engine/WiredIO/Hosting/WiredServer.cs +++ b/Engine/WiredIO/Hosting/WiredServer.cs @@ -2,7 +2,7 @@ using System.Net.Security; using System.Reflection; using System.Security.Cryptography.X509Certificates; - +using GenHTTP.Adapters.WiredIO; using GenHTTP.Api.Content; using GenHTTP.Api.Infrastructure; using GenHTTP.Engine.Shared.Infrastructure; @@ -73,7 +73,7 @@ private WiredApp Spawn(Action var app = builder.Build(); - app.Run(); // todo: adapter goes here (app.BuildPipeline) + app.BuildPipeline(Handler, this); applicationHook?.Invoke(app); From 7a71acd764e337e2ede30887b2dc10e765b3c282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20N=C3=A4geli?= Date: Wed, 22 Oct 2025 07:38:35 +0200 Subject: [PATCH 3/5] Switch to express, started mapping --- Adapters/WiredIO/Adapter.cs | 35 ++-- .../WiredIO/GenHTTP.Adapters.WiredIO.csproj | 2 +- Adapters/WiredIO/Mapping/Bridge.cs | 31 ++-- Adapters/WiredIO/Server/ImplicitServer.cs | 3 +- Adapters/WiredIO/Types/ClientConnection.cs | 18 +-- Adapters/WiredIO/Types/Request.cs | 67 ++++---- Engine/WiredIO/GenHTTP.Engine.WiredIO.csproj | 28 ---- Engine/WiredIO/Host.cs | 24 --- Engine/WiredIO/Hosting/WiredEndpoint.cs | 36 ----- Engine/WiredIO/Hosting/WiredEndpoints.cs | 5 - Engine/WiredIO/Hosting/WiredServer.cs | 151 ------------------ Engine/WiredIO/Hosting/WiredServerBuilder.cs | 27 ---- Engine/WiredIO/Server.cs | 28 ---- GenHTTP.slnx | 1 - 14 files changed, 71 insertions(+), 385 deletions(-) delete mode 100644 Engine/WiredIO/GenHTTP.Engine.WiredIO.csproj delete mode 100644 Engine/WiredIO/Host.cs delete mode 100644 Engine/WiredIO/Hosting/WiredEndpoint.cs delete mode 100644 Engine/WiredIO/Hosting/WiredEndpoints.cs delete mode 100644 Engine/WiredIO/Hosting/WiredServer.cs delete mode 100644 Engine/WiredIO/Hosting/WiredServerBuilder.cs delete mode 100644 Engine/WiredIO/Server.cs diff --git a/Adapters/WiredIO/Adapter.cs b/Adapters/WiredIO/Adapter.cs index 799795c24..b0aa728a6 100644 --- a/Adapters/WiredIO/Adapter.cs +++ b/Adapters/WiredIO/Adapter.cs @@ -7,39 +7,24 @@ using GenHTTP.Modules.ErrorHandling; using GenHTTP.Modules.IO; -using Wired.IO.App; -using Wired.IO.Http11.Context; +using Wired.IO.Builder; +using Wired.IO.Http11Express; +using Wired.IO.Http11Express.Context; namespace GenHTTP.Adapters.WiredIO; public static class Adapter { - /// - /// Registers the given handler to respond to requests to the specified path. - /// - /// The application to add the mapping to - /// The path to register the handler for - /// The handler to be registered - /// An object that will be informed about handled requests and any error - public static void Map(this WiredApp app, string path, IHandlerBuilder handler, IServerCompanion? companion = null) - => Map(app, path, handler.Build(), companion); + // ToDo: IBaseRequest and IBaseResponse do not feature basic access (such as headers), so we cannot be generic here - /// - /// Registers the given handler to respond to requests to the specified path. - /// - /// The application to add the mapping to - /// The path to register the handler for - /// The handler to be registered - /// An object that will be informed about handled requests and any error - public static void Map(this WiredApp app, string path, IHandler handler, IServerCompanion? companion = null) - => app.Map(path + "/{*any}", async context => await Bridge.MapAsync(context, handler, companion: companion, registeredPath: path)); + public static void Map(this Builder, Http11ExpressContext> builder, string path, IHandlerBuilder handler, IServerCompanion? companion = null) + => Map(builder, path, handler.Build(), companion); - public static void BuildPipeline(this WiredApp app, IHandlerBuilder handler, IServer server) - => app.BuildPipeline(handler, server); - - public static void BuildPipeline(this WiredApp app, IHandler handler, IServer server) - => app.BuildPipeline(new List, Task>>(), context => Bridge.MapAsync(context, handler, server)); + public static void Map(this Builder, 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)); + } /// /// Enables default features on the given handler. This should be used on the diff --git a/Adapters/WiredIO/GenHTTP.Adapters.WiredIO.csproj b/Adapters/WiredIO/GenHTTP.Adapters.WiredIO.csproj index cfe4eeebb..20e3b0ece 100644 --- a/Adapters/WiredIO/GenHTTP.Adapters.WiredIO.csproj +++ b/Adapters/WiredIO/GenHTTP.Adapters.WiredIO.csproj @@ -11,7 +11,7 @@ - + diff --git a/Adapters/WiredIO/Mapping/Bridge.cs b/Adapters/WiredIO/Mapping/Bridge.cs index bce16a6a9..2456ac9f1 100644 --- a/Adapters/WiredIO/Mapping/Bridge.cs +++ b/Adapters/WiredIO/Mapping/Bridge.cs @@ -5,20 +5,22 @@ using GenHTTP.Api.Infrastructure; using GenHTTP.Api.Protocol; -using Wired.IO.Http11.Context; +using Wired.IO.Http11Express.Context; + +using WR = Wired.IO.Protocol.Response; namespace GenHTTP.Adapters.WiredIO.Mapping; public static class Bridge { - public static async ValueTask MapAsync(Http11Context context, IHandler handler, IServer? server = null, IServerCompanion? companion = null, string? registeredPath = null) + public static async Task MapAsync(Http11ExpressContext context, Func next, IHandler handler, IServerCompanion? companion = null, string? registeredPath = null) { - var actualServer = server ?? new ImplicitServer(handler, companion); + var server = new ImplicitServer(handler, companion); try { - using var request = new Request(actualServer, context); + using var request = new Request(server, context.Request); if (registeredPath != null) { @@ -27,33 +29,34 @@ public static async ValueTask MapAsync(Http11Context context, IHandler handler, using var response = await handler.HandleAsync(request); - if (response == null) + if (response != null) { - context.Response.StatusCode = 404; + await WriteAsync(response, context); + + server.Companion?.OnRequestHandled(request, response); } else { - await WriteAsync(response, context); - - actualServer.Companion?.OnRequestHandled(request, response); + await next(context); } } catch (Exception e) { - actualServer.Companion?.OnServerError(ServerErrorScope.ServerConnection, context.Connection.RemoteIpAddress, e); + // todo: cannot tell the IP of the client in wired + server.Companion?.OnServerError(ServerErrorScope.ServerConnection, null, e); throw; } } - private static async ValueTask WriteAsync(IResponse response, HttpContext context) + private static async ValueTask WriteAsync(IResponse response, Http11ExpressContext context) { - var target = context.Response; + var target = context.Respond(); - target.StatusCode = response.Status.RawStatus; + target.Status((WR.ResponseStatus)response.Status.RawStatus); foreach (var header in response.Headers) { - target.Headers.Append(header.Key, header.Value); + target.Header(header.Key, header.Value); } if (response.Modified != null) diff --git a/Adapters/WiredIO/Server/ImplicitServer.cs b/Adapters/WiredIO/Server/ImplicitServer.cs index 77dffdd5e..0d688b7df 100644 --- a/Adapters/WiredIO/Server/ImplicitServer.cs +++ b/Adapters/WiredIO/Server/ImplicitServer.cs @@ -17,6 +17,7 @@ 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; } @@ -48,7 +49,7 @@ public ImplicitServer(IHandler handler, IServerCompanion? companion) public ValueTask DisposeAsync() => new(); - public ValueTask StartAsync() => throw new InvalidOperationException("Server is managed by ASP.NET Core and cannot be started"); + public ValueTask StartAsync() => throw new InvalidOperationException("Server is managed by WiredIO and cannot be started"); #endregion diff --git a/Adapters/WiredIO/Types/ClientConnection.cs b/Adapters/WiredIO/Types/ClientConnection.cs index f84526a67..d90e3b2f3 100644 --- a/Adapters/WiredIO/Types/ClientConnection.cs +++ b/Adapters/WiredIO/Types/ClientConnection.cs @@ -4,6 +4,8 @@ using GenHTTP.Api.Infrastructure; using GenHTTP.Api.Protocol; +using Wired.IO.Http11Express.Request; + namespace GenHTTP.Adapters.WiredIO.Types; public sealed class ClientConnection : IClientConnection @@ -11,28 +13,26 @@ public sealed class ClientConnection : IClientConnection #region Get-/Setters - public IPAddress IPAddress => Info.RemoteIpAddress ?? throw new InvalidOperationException("Remote client IP address is not known"); + public IPAddress IPAddress => throw new InvalidOperationException("Remote client IP address is not known"); public ClientProtocol? Protocol { get; } - public string? Host => Request.Host.HasValue ? Request.Host.Value : null; - - public X509Certificate? Certificate => Info.ClientCertificate; + public string? Host => Request.Headers.GetValueOrDefault("Host"); - private ConnectionInfo Info { get; } + public X509Certificate? Certificate => null; - private HttpRequest Request { get; } + private IExpressRequest Request { get; } #endregion #region Initialization - public ClientConnection(ConnectionInfo info, HttpRequest request) + public ClientConnection(IExpressRequest request) { - Info = info; Request = request; - Protocol = (request.IsHttps) ? ClientProtocol.Https : ClientProtocol.Http; + // todo: wired does not expose this information + Protocol = ClientProtocol.Http; } #endregion diff --git a/Adapters/WiredIO/Types/Request.cs b/Adapters/WiredIO/Types/Request.cs index 708c78a54..0ef60189c 100644 --- a/Adapters/WiredIO/Types/Request.cs +++ b/Adapters/WiredIO/Types/Request.cs @@ -1,13 +1,10 @@ using GenHTTP.Api.Infrastructure; using GenHTTP.Api.Protocol; using GenHTTP.Api.Routing; -using GenHTTP.Engine.Shared.Types; - -using Wired.IO.Http11.Context; -using HttpProtocol = GenHTTP.Api.Protocol.HttpProtocol; +using GenHTTP.Engine.Shared.Types; -using WiredRequest = Wired.IO.Http11.Request.IRequest; +using Wired.IO.Http11Express.Request; namespace GenHTTP.Adapters.WiredIO.Types; @@ -23,8 +20,6 @@ public sealed class Request : IRequest private Headers? _Headers; - private readonly IEndPoint? _EndPoint; - #region Get-/Setters public IRequestProperties Properties @@ -34,7 +29,7 @@ public IRequestProperties Properties public IServer Server { get; } - public IEndPoint EndPoint => _EndPoint ?? throw new InvalidOperationException("EndPoint is not available as it is managed by ASP.NET Core"); + public IEndPoint EndPoint => throw new InvalidOperationException("EndPoint is not available as it is managed by WiredIO"); public IClientConnection Client { get; } @@ -56,65 +51,67 @@ public IRequestProperties Properties public IRequestQuery Query { - get { return _Query ??= new Query(Context); } + get { return _Query ??= new Query(InnerRequest); } } public ICookieCollection Cookies { - get { return _Cookies ??= new Cookies(Context); } + get { return _Cookies ??= new Cookies(InnerRequest); } } public IForwardingCollection Forwardings => _Forwardings; public IHeaderCollection Headers { - get { return _Headers ??= new Headers(Context); } + get { return _Headers ??= new Headers(InnerRequest); } } - public Stream Content => Context.BodyReader.AsStream(true); + // todo: this is quite inefficient + public Stream Content => (InnerRequest.Content != null) ? new MemoryStream(InnerRequest.Content) : Stream.Null; - public FlexibleContentType? ContentType => (Context.ContentType != null) ? new(Context.ContentType) : null; + public FlexibleContentType? ContentType + { + get + { + if (InnerRequest.Headers.TryGetValue("Content-Type", out var contentType)) + { + return FlexibleContentType.Parse(contentType); + } - private WiredRequest Context { get; } + return null; + } + } + + private IExpressRequest InnerRequest { get; } #endregion #region Initialization - public Request(IServer server, Http11Context context) + public Request(IServer server, IExpressRequest request) { Server = server; - Context = context.Request; + InnerRequest = request; - ProtocolType = Context.ConnectionType switch - { - "HTTP/1.0" => HttpProtocol.Http10, - "HTTP/1.1" => HttpProtocol.Http11, - "HTTP/2" => HttpProtocol.Http2, - "HTTP/3" => HttpProtocol.Http3, - _ => HttpProtocol.Http11 - }; + // todo: no API provided by wired + ProtocolType = HttpProtocol.Http11; - Method = FlexibleRequestMethod.Get(Context.Method); - Target = new RoutingTarget(WebPath.FromString(Context.Path)); + Method = FlexibleRequestMethod.Get(request.HttpMethod); + Target = new RoutingTarget(WebPath.FromString(request.Route)); - if (context.Request.Headers.TryGetValue("forwarded", out var forwardings)) + if (request.Headers.TryGetValue("forwarded", out var entry)) { - foreach (var entry in forwardings) - { - if (entry != null) _Forwardings.Add(entry); - } + _Forwardings.Add(entry); } else { _Forwardings.TryAddLegacy(Headers); } - LocalClient = new ClientConnection(context.Connection, context.Request); - - Client = _Forwardings.DetermineClient(context.Connection.ClientCertificate) ?? LocalClient; + LocalClient = new ClientConnection(request); - _EndPoint = Server.EndPoints.FirstOrDefault(e => e.Port == context.Connection.LocalPort); + // todo: potential client certificate is not exposed by wired + Client = _Forwardings.DetermineClient(null) ?? LocalClient; } #endregion diff --git a/Engine/WiredIO/GenHTTP.Engine.WiredIO.csproj b/Engine/WiredIO/GenHTTP.Engine.WiredIO.csproj deleted file mode 100644 index c5004f1b2..000000000 --- a/Engine/WiredIO/GenHTTP.Engine.WiredIO.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - - net9.0 - - Web server engine based on Wired.IO that can be used to develop applications using the GenHTTP development framework - HTTP Embedded Webserver Server Library C# Wired.IO Engine - - - - - - - - - - - - - - - - - - - - diff --git a/Engine/WiredIO/Host.cs b/Engine/WiredIO/Host.cs deleted file mode 100644 index 91b50b748..000000000 --- a/Engine/WiredIO/Host.cs +++ /dev/null @@ -1,24 +0,0 @@ -using GenHTTP.Api.Infrastructure; - -using GenHTTP.Engine.Shared.Hosting; -using Wired.IO.App; -using Wired.IO.Builder; -using Wired.IO.Http11; -using Wired.IO.Http11.Context; - -namespace GenHTTP.Engine.WiredIO; - -public static class Host -{ - - /// - /// Provides a new server host that can be used to run a - /// server instance of the GenHTTP webserver. - /// - /// An action invoked with the pre-configured web application builder, allowing to customize the underlying ASP.NET app - /// An action invoked with the created application instance, allowing to customize the underlying ASP.NET app - /// The host which can be used to run a server instance - public static IServerHost Create(Action>? configurationHook = null, Action>? applicationHook = null) - => new ServerHost(Server.Create(configurationHook, applicationHook)); - -} diff --git a/Engine/WiredIO/Hosting/WiredEndpoint.cs b/Engine/WiredIO/Hosting/WiredEndpoint.cs deleted file mode 100644 index 55d4520ed..000000000 --- a/Engine/WiredIO/Hosting/WiredEndpoint.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Net; -using GenHTTP.Api.Infrastructure; - -namespace GenHTTP.Engine.WiredIO.Hosting; - -public sealed class WiredEndpoint : IEndPoint -{ - - #region Get-/Setters - - public IPAddress? Address { get; } - - public ushort Port { get; } - - public bool Secure { get; } - - #endregion - - #region Initialization - - public WiredEndpoint(IPAddress? address, ushort port, bool secure) - { - Address = address; - Port = port; - Secure = secure; - } - - #endregion - - #region Lifecycle - - public void Dispose() { } - - #endregion - -} diff --git a/Engine/WiredIO/Hosting/WiredEndpoints.cs b/Engine/WiredIO/Hosting/WiredEndpoints.cs deleted file mode 100644 index cab44a504..000000000 --- a/Engine/WiredIO/Hosting/WiredEndpoints.cs +++ /dev/null @@ -1,5 +0,0 @@ -using GenHTTP.Api.Infrastructure; - -namespace GenHTTP.Engine.WiredIO.Hosting; - -public sealed class WiredEndpoints : List, IEndPointCollection; diff --git a/Engine/WiredIO/Hosting/WiredServer.cs b/Engine/WiredIO/Hosting/WiredServer.cs deleted file mode 100644 index 3014915fc..000000000 --- a/Engine/WiredIO/Hosting/WiredServer.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System.Net; -using System.Net.Security; -using System.Reflection; -using System.Security.Cryptography.X509Certificates; -using GenHTTP.Adapters.WiredIO; -using GenHTTP.Api.Content; -using GenHTTP.Api.Infrastructure; -using GenHTTP.Engine.Shared.Infrastructure; - -using Wired.IO.App; -using Wired.IO.Builder; -using Wired.IO.Http11; -using Wired.IO.Http11.Context; - -namespace GenHTTP.Engine.WiredIO.Hosting; - -internal sealed class WiredServer : IServer -{ - - #region Get-/Setters - - public string Version { get; } - - public bool Running { get; private set; } - - public bool Development { get; } - - public IEndPointCollection EndPoints { get; } - - public IServerCompanion? Companion { get; } - - public IHandler Handler { get; } - - private ServerConfiguration Configuration { get; } - - private WiredApp Application { get; } - - #endregion - - #region Initialization - - internal WiredServer(IServerCompanion? companion, ServerConfiguration configuration, IHandler handler, Action>? configurationHook, Action>? applicationHook) - { - Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "(n/a)"; - - Companion = companion; - Configuration = configuration; - - Development = configuration.DevelopmentMode; - - Handler = handler; - - var endpoints = new WiredEndpoints(); - - endpoints.AddRange(configuration.EndPoints.Select(e => new WiredEndpoint(e.Address, e.Port, e.Security is not null))); - - EndPoints = endpoints; - - Application = Spawn(configurationHook, applicationHook); - } - - #endregion - - #region Functionality - - private WiredApp Spawn(Action>? configurationHook, Action>? applicationHook) - { - var builder = WiredApp.CreateBuilder(); - - Configure(builder); - - configurationHook?.Invoke(builder); - - var app = builder.Build(); - - app.BuildPipeline(Handler, this); - - applicationHook?.Invoke(app); - - return app; - } - - public async ValueTask StartAsync() - { - await Handler.PrepareAsync(); - - await Application.StartAsync(); - - Running = true; - } - - private void Configure(Builder builder) - { - foreach (var endpoint in Configuration.EndPoints) - { - builder.Endpoint(endpoint.Address ?? IPAddress.Any, endpoint.Port); - } - - var security = Secure(Configuration.EndPoints); - - if (security != null) - { - builder.UseTls(security); - } - } - - private static SslServerAuthenticationOptions? Secure(IEnumerable endpoints) - { - foreach (var endpoint in endpoints) - { - // todo: wired does not allow security-by-endpoint - if (endpoint.Security != null) - { - var security = endpoint.Security; - var validator = security.CertificateValidator; - - return new SslServerAuthenticationOptions() - { - EnabledSslProtocols = security.Protocols, - ServerCertificateSelectionCallback = (_, hostName) => security.CertificateProvider.Provide(hostName)!, - ClientCertificateRequired = validator?.RequireCertificate ?? false, - CertificateRevocationCheckMode = validator?.RevocationCheck ?? X509RevocationMode.NoCheck, - RemoteCertificateValidationCallback = (_, certificate, chain, sslPolicyErrors) => validator == null || validator.Validate(certificate, chain, sslPolicyErrors) - }; - } - } - - return null; - } - - #endregion - - #region Lifecycle - - private bool _Disposed; - - public async ValueTask DisposeAsync() - { - if (!_Disposed) - { - await Application.StopAsync(); - - Application.Dispose(); - - _Disposed = true; - } - } - - #endregion - -} diff --git a/Engine/WiredIO/Hosting/WiredServerBuilder.cs b/Engine/WiredIO/Hosting/WiredServerBuilder.cs deleted file mode 100644 index ddc812e76..000000000 --- a/Engine/WiredIO/Hosting/WiredServerBuilder.cs +++ /dev/null @@ -1,27 +0,0 @@ -using GenHTTP.Api.Content; -using GenHTTP.Api.Infrastructure; - -using GenHTTP.Engine.Shared.Infrastructure; - -using Wired.IO.App; -using Wired.IO.Builder; -using Wired.IO.Http11; -using Wired.IO.Http11.Context; - -namespace GenHTTP.Engine.WiredIO.Hosting; - -public sealed class WiredServerBuilder : ServerBuilder -{ - private readonly Action>? _ConfigurationHook; - - private readonly Action>? _ApplicationHook; - - public WiredServerBuilder(Action>? configurationHook, Action>? applicationHook) - { - _ConfigurationHook = configurationHook; - _ApplicationHook = applicationHook; - } - - protected override IServer Build(IServerCompanion? companion, ServerConfiguration config, IHandler handler) => new WiredServer(companion, config, handler, _ConfigurationHook, _ApplicationHook); - -} diff --git a/Engine/WiredIO/Server.cs b/Engine/WiredIO/Server.cs deleted file mode 100644 index 45e2aeeba..000000000 --- a/Engine/WiredIO/Server.cs +++ /dev/null @@ -1,28 +0,0 @@ -using GenHTTP.Api.Infrastructure; - -using GenHTTP.Engine.WiredIO.Hosting; - -using Wired.IO.App; -using Wired.IO.Builder; -using Wired.IO.Http11; -using Wired.IO.Http11.Context; - -namespace GenHTTP.Engine.WiredIO; - -/// -/// Allows to create server instances. -/// -public static class Server -{ - - /// - /// Create a new, configurable server instance with - /// default values. - /// - /// An action invoked with the pre-configured web application builder, allowing to customize the underlying ASP.NET app - /// An action invoked with the created application instance, allowing to customize the underlying ASP.NET app - /// The builder to create the instance - public static IServerBuilder Create(Action>? configurationHook = null, Action>? applicationHook = null) - => new WiredServerBuilder(configurationHook, applicationHook); - -} diff --git a/GenHTTP.slnx b/GenHTTP.slnx index 1c38f7f1d..d7caf9238 100644 --- a/GenHTTP.slnx +++ b/GenHTTP.slnx @@ -10,7 +10,6 @@ - From ce44976474295a634f6db6b32d9b0b835a27eaf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20N=C3=A4geli?= Date: Wed, 22 Oct 2025 14:08:20 +0200 Subject: [PATCH 4/5] First error-free mapping draft --- Adapters/WiredIO/Mapping/Bridge.cs | 35 ++++++++----------- Adapters/WiredIO/Types/Cookies.cs | 21 ----------- Adapters/WiredIO/Types/Headers.cs | 26 ++++++-------- Adapters/WiredIO/Types/MappedContent.cs | 27 +++++++++++++++ Adapters/WiredIO/Types/Query.cs | 46 +++++++++++++++---------- Adapters/WiredIO/Types/Request.cs | 16 +++++++-- 6 files changed, 94 insertions(+), 77 deletions(-) delete mode 100644 Adapters/WiredIO/Types/Cookies.cs create mode 100644 Adapters/WiredIO/Types/MappedContent.cs diff --git a/Adapters/WiredIO/Mapping/Bridge.cs b/Adapters/WiredIO/Mapping/Bridge.cs index 2456ac9f1..b33ef8655 100644 --- a/Adapters/WiredIO/Mapping/Bridge.cs +++ b/Adapters/WiredIO/Mapping/Bridge.cs @@ -16,6 +16,13 @@ public static class Bridge public static async Task MapAsync(Http11ExpressContext context, Func 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 @@ -31,7 +38,7 @@ public static async Task MapAsync(Http11ExpressContext context, Func, ICookieCollection -{ - - public Cookies(HttpRequest request) - { - foreach (var cookie in request.Cookies) - { - Add(cookie.Key, new(cookie.Key, cookie.Value)); - } - } - - public void Dispose() - { - - } - -} diff --git a/Adapters/WiredIO/Types/Headers.cs b/Adapters/WiredIO/Types/Headers.cs index 2aa82f8db..e7d13fa31 100644 --- a/Adapters/WiredIO/Types/Headers.cs +++ b/Adapters/WiredIO/Types/Headers.cs @@ -1,6 +1,9 @@ using System.Collections; + using GenHTTP.Api.Protocol; +using Wired.IO.Http11Express.Request; + namespace GenHTTP.Adapters.WiredIO.Types; public sealed class Headers : IHeaderCollection @@ -14,9 +17,9 @@ public sealed class Headers : IHeaderCollection public bool TryGetValue(string key, out string value) { - if (Request.Headers.TryGetValue(key, out var strings)) + if (Request.Headers.TryGetValue(key, out var found)) { - value = strings.FirstOrDefault() ?? string.Empty; + value = found; return true; } @@ -24,7 +27,7 @@ public bool TryGetValue(string key, out string value) return false; } - public string this[string key] => Request.Headers[key].FirstOrDefault() ?? string.Empty; + public string this[string key] => ContainsKey(key) ? Request.Headers[key] : string.Empty; public IEnumerable Keys => Request.Headers.Keys; @@ -34,21 +37,18 @@ public IEnumerable Values { foreach (var entry in Request.Headers) { - foreach (var value in entry.Value) - { - if (value != null) yield return value; - } + yield return entry.Value; } } } - private HttpRequest Request { get; } + private IExpressRequest Request { get; } #endregion #region Initialization - public Headers(HttpRequest request) + public Headers(IExpressRequest request) { Request = request; } @@ -61,13 +61,7 @@ public IEnumerator> GetEnumerator() { foreach (var entry in Request.Headers) { - foreach (var stringEntry in entry.Value) - { - if (stringEntry != null) - { - yield return new(entry.Key, stringEntry); - } - } + yield return new(entry.Key, entry.Value); } } diff --git a/Adapters/WiredIO/Types/MappedContent.cs b/Adapters/WiredIO/Types/MappedContent.cs new file mode 100644 index 000000000..001f6a9c1 --- /dev/null +++ b/Adapters/WiredIO/Types/MappedContent.cs @@ -0,0 +1,27 @@ +using System.IO.Pipelines; + +using GenHTTP.Api.Protocol; + +using Wired.IO.Http11Express.Response.Content; + +namespace GenHTTP.Adapters.WiredIO.Types; + +public class MappedContent(IResponse source) : IExpressResponseContent +{ + + public ulong? Length => source.ContentLength; + + public void Write(PipeWriter writer) + { + if (source.Content != null) + { + // todo: this is bad + using var stream = writer.AsStream(); + + source.Content.WriteAsync(stream, 4096).AsTask().GetAwaiter().GetResult(); + + writer.FlushAsync().AsTask().GetAwaiter().GetResult(); + } + } + +} diff --git a/Adapters/WiredIO/Types/Query.cs b/Adapters/WiredIO/Types/Query.cs index f189d9160..396f6c6ca 100644 --- a/Adapters/WiredIO/Types/Query.cs +++ b/Adapters/WiredIO/Types/Query.cs @@ -1,6 +1,8 @@ using System.Collections; + using GenHTTP.Api.Protocol; -using Wired.IO.Http11.Context; + +using Wired.IO.Http11Express.Request; namespace GenHTTP.Adapters.WiredIO.Types; @@ -9,15 +11,15 @@ public sealed class Query : IRequestQuery #region Get-/Setters - public int Count => Request.Query.Count; + public int Count => Request.QueryParameters?.Count ?? 0; - public bool ContainsKey(string key) => Request.Query.ContainsKey(key); + public bool ContainsKey(string key) => Request.QueryParameters?.ContainsKey(key) ?? false; public bool TryGetValue(string key, out string value) { - if (Request.Query.TryGetValue(key, out var stringValue)) + if (Request.QueryParameters?.TryGetValue(key, out var stringValue) ?? false) { - value = stringValue.FirstOrDefault() ?? string.Empty; + value = stringValue; return true; } @@ -25,31 +27,42 @@ public bool TryGetValue(string key, out string value) return false; } - public string this[string key] => Request.Query[key][0] ?? string.Empty; + public string this[string key] + { + get + { + if (Request.QueryParameters?.TryGetValue(key, out var stringValue) ?? false) + { + return stringValue; + } + + return string.Empty; + } + } - public IEnumerable Keys => Request.Query.Keys; + public IEnumerable Keys => Request.QueryParameters?.Keys ?? Enumerable.Empty(); public IEnumerable Values { get { - foreach (var entry in Request.Query) + if (Request.QueryParameters != null) { - foreach (var value in entry.Value) + foreach (var entry in Request.QueryParameters) { - if (value != null) yield return value; + yield return entry.Value; } } } } - private Http11Context Request { get; } + private IExpressRequest Request { get; } #endregion #region Initialization - public Query(Http11Context request) + public Query(IExpressRequest request) { Request = request; } @@ -60,14 +73,11 @@ public Query(Http11Context request) public IEnumerator> GetEnumerator() { - foreach (var entry in Request.Query) + if (Request.QueryParameters != null) { - foreach (var stringEntry in entry.Value) + foreach (var entry in Request.QueryParameters) { - if (stringEntry != null) - { - yield return new(entry.Key, stringEntry); - } + yield return new(entry.Key, entry.Value); } } } diff --git a/Adapters/WiredIO/Types/Request.cs b/Adapters/WiredIO/Types/Request.cs index 0ef60189c..3b4030c8d 100644 --- a/Adapters/WiredIO/Types/Request.cs +++ b/Adapters/WiredIO/Types/Request.cs @@ -14,7 +14,7 @@ public sealed class Request : IRequest private Query? _Query; - private Cookies? _Cookies; + private ICookieCollection? _Cookies; private readonly ForwardingCollection _Forwardings = new(); @@ -56,7 +56,7 @@ public IRequestQuery Query public ICookieCollection Cookies { - get { return _Cookies ??= new Cookies(InnerRequest); } + get { return _Cookies ??= FetchCookies(InnerRequest); } } public IForwardingCollection Forwardings => _Forwardings; @@ -114,6 +114,18 @@ public Request(IServer server, IExpressRequest request) Client = _Forwardings.DetermineClient(null) ?? LocalClient; } + private CookieCollection FetchCookies(IExpressRequest request) + { + var cookies = new CookieCollection(); + + if (request.Headers.TryGetValue("Cookie", out var header)) + { + cookies.Add(header); + } + + return cookies; + } + #endregion #region Functionality From 740583e8337583ab1879078840c4d57088efb96e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20N=C3=A4geli?= Date: Wed, 22 Oct 2025 14:12:52 +0200 Subject: [PATCH 5/5] =?UTF-8?q?Try=20to=20override=20=C3=82'TargetFramewor?= =?UTF-8?q?ks'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Adapters/WiredIO/GenHTTP.Adapters.WiredIO.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Adapters/WiredIO/GenHTTP.Adapters.WiredIO.csproj b/Adapters/WiredIO/GenHTTP.Adapters.WiredIO.csproj index 20e3b0ece..ea1498f5f 100644 --- a/Adapters/WiredIO/GenHTTP.Adapters.WiredIO.csproj +++ b/Adapters/WiredIO/GenHTTP.Adapters.WiredIO.csproj @@ -2,7 +2,7 @@ - net9.0 + net9.0 Adapter to run GenHTTP handlers within an Wired.IO app. Wired.IO Adapter GenHTTP