diff options
Diffstat (limited to 'lib')
26 files changed, 556 insertions, 407 deletions
diff --git a/lib/Net.Http/src/Core/HttpServerBase.cs b/lib/Net.Http/src/Core/HttpServerBase.cs index 1ccace9..0eb5cef 100644 --- a/lib/Net.Http/src/Core/HttpServerBase.cs +++ b/lib/Net.Http/src/Core/HttpServerBase.cs @@ -115,7 +115,7 @@ namespace VNLib.Net.Http /// <summary> /// Gets a value indicating whether the server is listening for connections /// </summary> - public bool Running { get; private set; } + public bool Running => Transports.Any(static t => t.Running); /// <summary> /// Cached supported compression methods @@ -284,22 +284,11 @@ namespace VNLib.Net.Http //Listen to connections on all transports async IEnumerable<Task> runTasks = Transports.Select(ListenAsync); - //Set running flag and will be reset when all listening tasks are done - Running = true; - //Calling WhenAll() will force the numeration and schedule listening tasks - return Task.WhenAll(runTasks) - .ContinueWith( - OnAllStopped, - CancellationToken.None, - TaskContinuationOptions.RunContinuationsAsynchronously, - TaskScheduler.Default - ); + return Task.WhenAll(runTasks); //Defer listening tasks to the task scheduler to avoid blocking this thread Task ListenAsync(ListenerState tp) => Task.Run(() => ListenWorkerDoWork(tp), cancellationToken); - - void OnAllStopped(Task _) => Running = false; } /* @@ -308,11 +297,22 @@ namespace VNLib.Net.Http private async Task ListenWorkerDoWork(ListenerState state) { state.Running = true; - - _config.ServerLog.Information("HTTP server {hc} listening for connections", GetHashCode()); + + if (_config.ServerLog.IsEnabled(LogLevel.Verbose)) + { + _config.ServerLog.Verbose( + format: "HTTP server {hc} listening for connections on {iface}", + GetHashCode(), + state.OriginServer + ); + } + else + { + _config.ServerLog.Information("HTTP server {hc} listening for connections", GetHashCode()); + } //Listen for connections until canceled - while (true) + do { try { @@ -331,7 +331,8 @@ namespace VNLib.Net.Http { _config.ServerLog.Error(ex); } - } + + } while (true); //Clear all caches before leaving to aid gc CacheHardClear(); diff --git a/lib/Net.Http/src/Core/HttpServerProcessing.cs b/lib/Net.Http/src/Core/HttpServerProcessing.cs index f5dbbc7..8594ea0 100644 --- a/lib/Net.Http/src/Core/HttpServerProcessing.cs +++ b/lib/Net.Http/src/Core/HttpServerProcessing.cs @@ -29,7 +29,6 @@ using System.Threading; using System.Net.Sockets; using System.Diagnostics; using System.Threading.Tasks; -using System.Collections.Generic; using System.Runtime.CompilerServices; using VNLib.Utils.Memory; @@ -386,16 +385,17 @@ namespace VNLib.Net.Http private async Task<bool> ProcessRequestAsync(ListenerState listenState, HttpContext context) { //Get the server root for the specified location or fallback to a wildcard host if one is selected - IWebRoot? root = listenState.Roots.GetValueOrDefault( - context.Request.State.Location.DnsSafeHost, - listenState.DefaultRoute - ); - - if (root == null) + + if (!listenState.Roots.TryGetValue(context.Request.State.Location.DnsSafeHost, out IWebRoot? root)) { - context.Respond(HttpStatusCode.NotFound); - //make sure control leaves - return true; + if (listenState.DefaultRoute is null) + { + context.Respond(HttpStatusCode.NotFound); + //make sure control leaves + return true; + } + + root = listenState.DefaultRoute; } //Check the expect header and return an early status code diff --git a/lib/Net.Http/src/HttpConfig.cs b/lib/Net.Http/src/HttpConfig.cs index aa6e34a..40e9f88 100644 --- a/lib/Net.Http/src/HttpConfig.cs +++ b/lib/Net.Http/src/HttpConfig.cs @@ -53,17 +53,11 @@ namespace VNLib.Net.Http /// <summary> /// Initializes a new instance of the <see cref="HttpConfig"/> struct /// </summary> - /// <param name="serverLog"></param> - /// <param name="memoryPool"></param> /// <param name="httpEncoding"></param> - public HttpConfig(ILogProvider serverLog, IHttpMemoryPool memoryPool, Encoding httpEncoding) + public HttpConfig(Encoding httpEncoding) { - ArgumentNullException.ThrowIfNull(serverLog); - ArgumentNullException.ThrowIfNull(memoryPool); ArgumentNullException.ThrowIfNull(httpEncoding); - ServerLog = serverLog; - MemoryPool = memoryPool; HttpEncoding = httpEncoding; //Init pre-encded segments @@ -77,12 +71,12 @@ namespace VNLib.Net.Http /// <summary> /// A log provider that all server related log entiries will be written to /// </summary> - public readonly ILogProvider ServerLog { get; init; } + public required readonly ILogProvider ServerLog { get; init; } /// <summary> /// Server memory pool to use for allocating buffers /// </summary> - public readonly IHttpMemoryPool MemoryPool { get; init; } + public required readonly IHttpMemoryPool MemoryPool { get; init; } /// <summary> /// The absolute request entity body size limit in bytes diff --git a/lib/Net.Messaging.FBM/src/Server/FBMListenerSessionParams.cs b/lib/Net.Messaging.FBM/src/Server/FBMListenerSessionParams.cs index 0b4fa5b..435284b 100644 --- a/lib/Net.Messaging.FBM/src/Server/FBMListenerSessionParams.cs +++ b/lib/Net.Messaging.FBM/src/Server/FBMListenerSessionParams.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Messaging.FBM @@ -36,27 +36,27 @@ namespace VNLib.Net.Messaging.FBM.Server /// The size of the buffer to use while reading data from the websocket /// in the listener loop /// </summary> - public readonly int RecvBufferSize { get; init; } + public required readonly int RecvBufferSize { get; init; } /// <summary> /// The size of the buffer to store <see cref="FBMMessageHeader"/> values in /// the <see cref="FBMRequestMessage"/> /// </summary> - public readonly int MaxHeaderBufferSize { get; init; } + public required readonly int MaxHeaderBufferSize { get; init; } /// <summary> /// The size of the internal message response buffer when /// not streaming /// </summary> - public readonly int ResponseBufferSize { get; init; } + public required readonly int ResponseBufferSize { get; init; } /// <summary> /// The FMB message header character encoding /// </summary> - public readonly Encoding HeaderEncoding { get; init; } + public required readonly Encoding HeaderEncoding { get; init; } /// <summary> /// The absolute maxium size (in bytes) message to process before /// closing the websocket connection. This value should be negotiaed /// by clients or hard-coded to avoid connection issues /// </summary> - public readonly int MaxMessageSize { get; init; } + public required readonly int MaxMessageSize { get; init; } } } diff --git a/lib/Net.Transport.SimpleTCP/src/TCPConfig.cs b/lib/Net.Transport.SimpleTCP/src/TCPConfig.cs index 9003e0d..f5c8893 100644 --- a/lib/Net.Transport.SimpleTCP/src/TCPConfig.cs +++ b/lib/Net.Transport.SimpleTCP/src/TCPConfig.cs @@ -39,44 +39,44 @@ namespace VNLib.Net.Transport.Tcp /// <summary> /// The <see cref="IPEndPoint"/> the listening socket will bind to /// </summary> - public readonly IPEndPoint LocalEndPoint { get; init; } + public required readonly IPEndPoint LocalEndPoint { get; init; } /// <summary> /// The log provider used to write logging information to /// </summary> - public readonly ILogProvider Log { get; init; } + public required readonly ILogProvider Log { get; init; } /// <summary> /// If TCP keepalive is enabled, the amount of time the connection is considered alive before another probe message is sent /// </summary> - public readonly int TcpKeepAliveTime { get; init; } + public required readonly int TcpKeepAliveTime { get; init; } /// <summary> /// If TCP keepalive is enabled, the amount of time the connection will wait for a keepalive message /// </summary> - public readonly int KeepaliveInterval { get; init; } + public required readonly int KeepaliveInterval { get; init; } /// <summary> /// Enables TCP keepalive /// </summary> - public readonly bool TcpKeepalive { get; init; } + public required readonly bool TcpKeepalive { get; init; } /// <summary> /// The maximum number of waiting WSA asynchronous socket accept operations /// </summary> - public readonly uint AcceptThreads { get; init; } + public required readonly uint AcceptThreads { get; init; } /// <summary> /// The maximum size (in bytes) the transport will buffer in /// the receiving pipeline. /// </summary> - public readonly int MaxRecvBufferData { get; init; } + public required readonly int MaxRecvBufferData { get; init; } /// <summary> /// The maximum number of allowed socket connections to this server /// </summary> - public readonly long MaxConnections { get; init; } + public required readonly long MaxConnections { get; init; } /// <summary> /// The listener socket backlog count /// </summary> - public readonly int BackLog { get; init; } + public required readonly int BackLog { get; init; } /// <summary> /// The <see cref="MemoryPool{T}"/> to allocate transport buffers from /// </summary> - public readonly MemoryPool<byte> BufferPool { get; init; } + public required readonly MemoryPool<byte> BufferPool { get; init; } /// <summary> /// <para> /// The maxium number of event objects that will be cached @@ -86,16 +86,16 @@ namespace VNLib.Net.Transport.Tcp /// WARNING: Setting this value too low will cause significant CPU overhead and GC load /// </para> /// </summary> - public readonly int CacheQuota { get; init; } + public required readonly int CacheQuota { get; init; } /// <summary> /// An optional callback invoked after the socket has been created /// for optional appliction specific socket configuration /// </summary> - public readonly Action<Socket>? OnSocketCreated { get; init; } + public required readonly Action<Socket>? OnSocketCreated { get; init; } /// <summary> /// Enables verbose logging of TCP operations using the <see cref="LogLevel.Verbose"/> /// level /// </summary> - public readonly bool DebugTcpLog { get; init; } + public required readonly bool DebugTcpLog { get; init; } } }
\ No newline at end of file diff --git a/lib/Net.Transport.SimpleTCP/src/TcpServer.cs b/lib/Net.Transport.SimpleTCP/src/TcpServer.cs index ad8987e..5a82298 100644 --- a/lib/Net.Transport.SimpleTCP/src/TcpServer.cs +++ b/lib/Net.Transport.SimpleTCP/src/TcpServer.cs @@ -66,19 +66,14 @@ namespace VNLib.Net.Transport.Tcp if (pipeOptions == null) { //Pool is required when using default pipe options - _ = config.BufferPool ?? throw new ArgumentException("Buffer pool argument cannot be null"); + ArgumentNullException.ThrowIfNull(config.BufferPool); } - _ = config.Log ?? throw new ArgumentException("Log argument is required"); + ArgumentNullException.ThrowIfNull(config.Log, nameof(config.Log)); + + ArgumentOutOfRangeException.ThrowIfLessThan(config.MaxRecvBufferData, 4096); + ArgumentOutOfRangeException.ThrowIfLessThan(config.AcceptThreads, 1u); - if (config.MaxRecvBufferData < 4096) - { - throw new ArgumentException("MaxRecvBufferData size must be at least 4096 bytes to avoid data pipeline pefromance issues"); - } - if (config.AcceptThreads < 1) - { - throw new ArgumentException("Accept thread count must be greater than 0"); - } if (config.AcceptThreads > Environment.ProcessorCount) { config.Log.Debug("Suggestion: Setting accept threads to {pc}", Environment.ProcessorCount); diff --git a/lib/Plugins.Essentials.ServiceStack/src/Construction/HttpServiceStackBuilder.cs b/lib/Plugins.Essentials.ServiceStack/src/Construction/HttpServiceStackBuilder.cs index 3ae2183..3240330 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/Construction/HttpServiceStackBuilder.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/Construction/HttpServiceStackBuilder.cs @@ -28,16 +28,18 @@ using System.Collections.Generic; using VNLib.Net.Http; using VNLib.Plugins.Runtime; - +using VNLib.Plugins.Essentials.ServiceStack.Plugins; namespace VNLib.Plugins.Essentials.ServiceStack.Construction { + /// <summary> /// A data structure used to build/create a <see cref="HttpServiceStack"/> /// around a <see cref="ServiceDomain"/> /// </summary> public sealed class HttpServiceStackBuilder { + private readonly ServiceBuilder _serviceBuilder = new(); /// <summary> /// Initializes a new <see cref="HttpServiceStack"/> that will @@ -47,8 +49,10 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction public HttpServiceStackBuilder() { } - private Action<ICollection<IServiceHost>>? _hostBuilder; - private Func<ServiceGroup, IHttpServer>? _getServers; + internal ServiceBuilder ServiceBuilder => _serviceBuilder; + + private Action<ServiceBuilder>? _hostBuilder; + private Func<IReadOnlyCollection<ServiceGroup>, IHttpServer[]>? _getServers; private Func<IPluginStack>? _getPlugins; private IManualPlugin[]? manualPlugins; private bool loadConcurrently; @@ -59,7 +63,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction /// </summary> /// <param name="hostBuilder">The callback method to build virtual hosts</param> /// <returns>The current instance for chaining</returns> - public HttpServiceStackBuilder WithDomain(Action<ICollection<IServiceHost>> hostBuilder) + public HttpServiceStackBuilder WithDomain(Action<ServiceBuilder> hostBuilder) { _hostBuilder = hostBuilder; return this; @@ -70,7 +74,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction /// </summary> /// <param name="getServers">A callback method that gets the http server implementation for the service group</param> /// <returns>The current instance for chaining</returns> - public HttpServiceStackBuilder WithHttp(Func<ServiceGroup, IHttpServer> getServers) + public HttpServiceStackBuilder WithHttp(Func<IReadOnlyCollection<ServiceGroup>, IHttpServer[]> getServers) { _getServers = getServers; return this; @@ -90,24 +94,34 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction /// <summary> /// Configures the stack to use the built-in http server implementation /// </summary> - /// <param name="transport">The transport builder callback function</param> + /// <param name="getTransports">The transport builder callback function</param> /// <param name="config">The http configuration structure used to initalize servers</param> /// <returns>The current instance for chaining</returns> - public HttpServiceStackBuilder WithBuiltInHttp(Func<ServiceGroup, ITransportProvider> transport, HttpConfig config) - { - return WithBuiltInHttp(transport, sg => config); - } + public HttpServiceStackBuilder WithBuiltInHttp(Func<IReadOnlyCollection<ServiceGroup>, HttpTransportMapping[]> getTransports, HttpConfig config) + => WithBuiltInHttp(getTransports, _ => config); /// <summary> /// Configures the stack to use the built-in http server implementation /// </summary> - /// <param name="transport">The transport builder callback function</param> + /// <param name="getBindings">A callback function that gets transport bindings for servie groups</param> /// <param name="configCallback">The http configuration builder callback method</param> /// <returns>The current instance for chaining</returns> - public HttpServiceStackBuilder WithBuiltInHttp(Func<ServiceGroup, ITransportProvider> transport, Func<ServiceGroup, HttpConfig> configCallback) - { - return WithHttp(sg => new HttpServer(configCallback(sg), transport(sg), sg.Hosts.Select(static p => p.Processor))); - } + public HttpServiceStackBuilder WithBuiltInHttp( + Func<IReadOnlyCollection<ServiceGroup>, HttpTransportMapping[]> getBindings, + Func<IReadOnlyCollection<ServiceGroup>, HttpConfig> configCallback + ) => WithHttp((sgs) => { + + HttpTransportBinding[] vhBindings = getBindings(sgs) + .Select(s => + { + IEnumerable<IWebRoot> procs = s.Hosts.Select(static s => s.Processor); + return new HttpTransportBinding(s.Transport, procs); + }) + .ToArray(); + + // A single built-in http server can service an entire domain + return [ new HttpServer(configCallback(sgs), vhBindings) ]; + }); /// <summary> /// Adds a collection of manual plugin instances to the stack. Every call @@ -139,27 +153,27 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction /// <exception cref="ArgumentNullException"></exception> public HttpServiceStack Build() { - _ = _hostBuilder ?? throw new ArgumentNullException("WithDomainBuilder", "You have not configured a service domain configuration callback"); _ = _getServers ?? throw new ArgumentNullException("WithHttp", "You have not configured a IHttpServer configuration callback"); + //Host builder callback is optional + _hostBuilder?.Invoke(ServiceBuilder); + //Inint the service domain ServiceDomain sd = new(); - if (!sd.BuildDomain(_hostBuilder)) - { - throw new ArgumentException("Failed to configure the service domain, you must expose at least one service host"); - } + sd.BuildDomain(_serviceBuilder); - LinkedList<IHttpServer> servers = new(); + //Get http servers from the user callback for the service domain, let the caller decide how to route them + IHttpServer[] servers = _getServers.Invoke(sd.ServiceGroups); - //enumerate hosts groups - foreach (ServiceGroup hosts in sd.ServiceGroups) + if (servers.Length == 0) { - //Create new server - IHttpServer server = _getServers.Invoke(hosts); + throw new ArgumentException("No service hosts were configured. You must define at least one virtual host for the domain"); + } - //Add server to internal list - servers.AddLast(server); + if(servers.Any(servers => servers is null)) + { + throw new ArgumentException("One or more servers were not initialized correctly. Check the server configuration callback"); } return new(servers, sd, GetPluginStack(sd)); diff --git a/lib/Plugins.Essentials.ServiceStack/src/IHostTransportInfo.cs b/lib/Plugins.Essentials.ServiceStack/src/Construction/HttpTransportMapping.cs index 5c663a9..c5fae41 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/IHostTransportInfo.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/Construction/HttpTransportMapping.cs @@ -1,12 +1,12 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack -* File: IHostTransportInfo.cs +* File: HttpTransportMapping.cs * -* IHostTransportInfo.cs is part of VNLib.Plugins.Essentials.ServiceStack which is part of the larger -* VNLib collection of libraries and utilities. +* HttpTransportMapping.cs is part of VNLib.Plugins.Essentials.ServiceStack which is +* part of the larger VNLib collection of libraries and utilities. * * VNLib.Plugins.Essentials.ServiceStack is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -22,26 +22,16 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -using System.Net; -using System.Security.Cryptography.X509Certificates; +using System.Collections.Generic; -namespace VNLib.Plugins.Essentials.ServiceStack +using VNLib.Net.Http; + +namespace VNLib.Plugins.Essentials.ServiceStack.Construction { /// <summary> - /// Represents the service host's network/transport - /// information including the optional certificate and - /// the endpoint to listen on + /// Represents a mapping of service hosts to a transport provider /// </summary> - public interface IHostTransportInfo - { - /// <summary> - /// Optional TLS certificate to use - /// </summary> - X509Certificate? Certificate { get; } - - /// <summary> - /// The endpoint to listen on - /// </summary> - IPEndPoint TransportEndpoint { get; } - } + /// <param name="Hosts">The collection of service hosts to map to transports</param> + /// <param name="Transport">The transport that will provide networking to the host collection</param> + public record class HttpTransportMapping(IEnumerable<IServiceHost> Hosts, ITransportProvider Transport); } diff --git a/lib/Plugins.Essentials.ServiceStack/src/Construction/IDomainBuilder.cs b/lib/Plugins.Essentials.ServiceStack/src/Construction/IDomainBuilder.cs index 19d2a96..9bccf54 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/Construction/IDomainBuilder.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/Construction/IDomainBuilder.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack @@ -22,9 +22,7 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -using System.IO; - -using VNLib.Utils.Logging; +using System; namespace VNLib.Plugins.Essentials.ServiceStack.Construction { @@ -34,19 +32,17 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction public interface IDomainBuilder { /// <summary> - /// Adds a single virtual host to the domain that must be configured. + /// Allows for defining a new virtual host for the domain by manually configuring it. /// </summary> - /// <param name="rootDirectory">The service root directory</param> - /// <param name="hooks">The virtual host event hook handler</param> - /// <param name="Logger">The log provider</param> - /// <returns>The <see cref="IVirtualHostBuilder"/> instance</returns> - IVirtualHostBuilder WithVirtualHost(DirectoryInfo rootDirectory, IVirtualHostHooks hooks, ILogProvider Logger); + /// <param name="builder">A callback function that passes the new host builder</param> + /// <returns>The current instance</returns> + IDomainBuilder WithServiceGroups(Action<IServiceGroupBuilder> builder); /// <summary> - /// Adds a single pre-configured virtual host to the domain + /// Adds a collection of hosts to the domain /// </summary> - /// <param name="config">The pre-configured virtual host configuration</param> - /// <returns>The current instance</returns> - IDomainBuilder WithVirtualHost(VirtualHostConfiguration config); + /// <param name="host"></param> + /// <returns></returns> + IDomainBuilder WithHosts(IServiceHost[] host); } } diff --git a/lib/Plugins.Essentials.ServiceStack/src/Construction/IServiceGroupBuilder.cs b/lib/Plugins.Essentials.ServiceStack/src/Construction/IServiceGroupBuilder.cs new file mode 100644 index 0000000..1ffeb5c --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/Construction/IServiceGroupBuilder.cs @@ -0,0 +1,60 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.ServiceStack +* File: IServiceGroupBuilder.cs +* +* IServiceGroupBuilder.cs is part of VNLib.Plugins.Essentials.ServiceStack which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.ServiceStack is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 2 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials.ServiceStack is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; + +using VNLib.Utils.Logging; + +namespace VNLib.Plugins.Essentials.ServiceStack.Construction +{ + /// <summary> + /// Allows for defining service groups for the service stack to manage + /// </summary> + public interface IServiceGroupBuilder + { + /// <summary> + /// Adds a single virtual host to the domain that must be configured. + /// </summary> + /// <param name="rootDirectory">The service root directory</param> + /// <param name="hooks">The virtual host event hook handler</param> + /// <param name="Logger">The log provider</param> + /// <returns>The <see cref="IVirtualHostBuilder"/> instance</returns> + IVirtualHostBuilder WithVirtualHost(DirectoryInfo rootDirectory, IVirtualHostHooks hooks, ILogProvider Logger); + + /// <summary> + /// Allows for defining a new virtual host for the domain by manually configuring it. + /// </summary> + /// <param name="builder">A callback function that passes the new host builder</param> + /// <returns>The current instance</returns> + IServiceGroupBuilder WithVirtualHost(Action<IVirtualHostBuilder> builder); + + /// <summary> + /// Adds a single pre-configured virtual host to the domain + /// </summary> + /// <param name="config">The pre-configured virtual host configuration</param> + /// <returns>The current instance</returns> + IServiceGroupBuilder WithVirtualHost(VirtualHostConfiguration config, object? userState); + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/Construction/ServiceBuilder.cs b/lib/Plugins.Essentials.ServiceStack/src/Construction/ServiceBuilder.cs new file mode 100644 index 0000000..2464d9b --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/Construction/ServiceBuilder.cs @@ -0,0 +1,79 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.ServiceStack +* File: HttpServiceStackBuilder.cs +* +* HttpServiceStackBuilder.cs is part of VNLib.Plugins.Essentials.ServiceStack which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.ServiceStack is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 2 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials.ServiceStack is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Linq; +using System.Collections.Generic; + + +namespace VNLib.Plugins.Essentials.ServiceStack.Construction +{ + /// <summary> + /// A data structure used to build groupings of service hosts used + /// to configure http servers. + /// </summary> + public sealed class ServiceBuilder + { + private readonly List<Action<ICollection<IServiceHost>>> _callbacks = []; + + /// <summary> + /// Adds callback function that will add a collection of service hosts + /// and passes a state paramter to the callback + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="state">The optional state parameter</param> + /// <param name="host">The host collection to add new service hosts to</param> + /// <returns>The current instance for chaining</returns> + public ServiceBuilder AddHostCollection<T>(T state, Action<ICollection<IServiceHost>, T> host) + => AddHostCollection(col => host.Invoke(col, state)); + + /// <summary> + /// Adds a callback function that will add a collection of service hosts + /// </summary> + /// <param name="host">The callback function to return the collection of hosts</param> + /// <returns>The current instance for chaining</returns> + public ServiceBuilder AddHostCollection(Action<ICollection<IServiceHost>> host) + { + _callbacks.Add(host); + return this; + } + + /// <summary> + /// Builds the <see cref="ServiceGroup"/> collection from the user + /// defined service host arrays + /// </summary> + /// <returns>The numeration that builds the service groups</returns> + internal IEnumerable<ServiceGroup> BuildGroups() + { + return _callbacks.Select(static cb => + { + LinkedList<IServiceHost> hosts = new(); + + cb.Invoke(hosts); + + return new ServiceGroup(hosts); + }); + } + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/Construction/SsBuilderExtensions.cs b/lib/Plugins.Essentials.ServiceStack/src/Construction/SsBuilderExtensions.cs index 4195553..62b5070 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/Construction/SsBuilderExtensions.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/Construction/SsBuilderExtensions.cs @@ -29,12 +29,12 @@ using System.Linq; using System.Collections.Frozen; using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Security.Cryptography.X509Certificates; using VNLib.Utils.Logging; using VNLib.Utils.Extensions; using VNLib.Net.Http; using VNLib.Plugins.Essentials.Middleware; +using VNLib.Plugins.Essentials.ServiceStack.Plugins; namespace VNLib.Plugins.Essentials.ServiceStack.Construction { @@ -46,14 +46,6 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction { /// <summary> - /// Creates a new <see cref="IDomainBuilder"/> instance to define your - /// virtual hosts using a built-in event processor type - /// </summary> - /// <param name="stack"></param> - /// <returns>The <see cref="IDomainBuilder"/> used to define your service domain</returns> - public static IDomainBuilder WithDomain(this HttpServiceStackBuilder stack) => WithDomain(stack, vhc => FromVirtualHostConfig(vhc.Clone())); - - /// <summary> /// Creates a new <see cref="IDomainBuilder"/> instance to define your /// virtual hosts with the supplied callback method /// </summary> @@ -62,167 +54,144 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction /// <returns>The service stack builder instance</returns> public static HttpServiceStackBuilder WithDomain(this HttpServiceStackBuilder stack, Action<IDomainBuilder> domainBuilder) { - domainBuilder(stack.WithDomain()); + domainBuilder(WithDomain(stack)); return stack; } /// <summary> - /// Creates a new <see cref="IDomainBuilder"/> with your custom <see cref="EventProcessor"/> type - /// that will be wrapped for runtime processing. + /// Creates a new <see cref="IDomainBuilder"/> instance to define your + /// virtual hosts using a built-in event processor type /// </summary> - /// <typeparam name="T"></typeparam> /// <param name="stack"></param> - /// <param name="callback">The custom event processor type</param> - /// <returns></returns> - public static IDomainBuilder WithDomain<T>(this HttpServiceStackBuilder stack, Func<VirtualHostConfiguration, T> callback) - where T : EventProcessor, IRuntimeServiceInjection - { - List<VirtualHostConfiguration> configs = new(); - DomainBuilder domains = new(configs, stack); - - //Add callback to capture this collection of configs when built - stack.AddHosts(() => configs.Select(c => new CustomServiceHost<T>(c.Clone(), callback(c))).ToArray()); - - return domains; - } - - /// <summary> - /// Adds a single <see cref="IHttpMiddleware"/> instance to the virtual host - /// </summary> - /// <param name="vhBuilder"></param> - /// <param name="middleware">The middleware instance to add</param> - /// <returns></returns> - public static IVirtualHostBuilder WithMiddleware(this IVirtualHostBuilder vhBuilder, IHttpMiddleware middleware) - { - vhBuilder.WithOption(c => c.CustomMiddleware.Add(middleware)); - return vhBuilder; - } - - /// <summary> - /// Adds multiple <see cref="IHttpMiddleware"/> instances to the virtual host - /// </summary> - /// <param name="vhBuilder"></param> - /// <param name="middleware">The array of middleware instances to add to the collection</param> - /// <returns></returns> - public static IVirtualHostBuilder WithMiddleware(this IVirtualHostBuilder vhBuilder, params IHttpMiddleware[] middleware) - { - vhBuilder.WithOption(c => Array.ForEach(middleware, m => c.CustomMiddleware.Add(m))); - return vhBuilder; - } - - - public static IVirtualHostBuilder WithLogger(this IVirtualHostBuilder vhBuilder, ILogProvider logger) - { - vhBuilder.WithOption(c => c.LogProvider = logger); - return vhBuilder; - } - - public static IVirtualHostBuilder WithEndpoint(this IVirtualHostBuilder vhBuilder, IPEndPoint endpoint) - { - vhBuilder.WithOption(c => c.TransportEndpoint = endpoint); - return vhBuilder; - } - - public static IVirtualHostBuilder WithTlsCertificate(this IVirtualHostBuilder vhBuilder, X509Certificate? cert) - { - vhBuilder.WithOption(c => c.Certificate = cert); - return vhBuilder; - } + /// <returns>The <see cref="IDomainBuilder"/> used to define your service domain</returns> + public static IDomainBuilder WithDomain(this HttpServiceStackBuilder stack) + => new DomainBuilder(stack.ServiceBuilder); - public static IVirtualHostBuilder WithHostname(this IVirtualHostBuilder virtualHostBuilder, string hostname) - { - virtualHostBuilder.WithOption(c => c.Hostname = hostname); - return virtualHostBuilder; - } - public static IVirtualHostBuilder WithDefaultFiles(this IVirtualHostBuilder vhBuidler, params string[] defaultFiles) + private sealed class DomainBuilder(ServiceBuilder svcBuilder) : IDomainBuilder { - return vhBuidler.WithDefaultFiles((IReadOnlyCollection<string>)defaultFiles); - } - - public static IVirtualHostBuilder WithDefaultFiles(this IVirtualHostBuilder vhBuidler, IReadOnlyCollection<string> defaultFiles) - { - vhBuidler.WithOption(c => c.DefaultFiles = defaultFiles); - return vhBuidler; - } + ///<inheritdoc/> + public IDomainBuilder WithServiceGroups(Action<IServiceGroupBuilder> builder) + { + svcBuilder.AddHostCollection((col) => + { + SvGroupBuilder group = new(); - public static IVirtualHostBuilder WithExcludedExtensions(this IVirtualHostBuilder vhBuilder, params string[] excludedExtensions) - { - return vhBuilder.WithExcludedExtensions(new HashSet<string>(excludedExtensions)); - } + builder(group); - public static IVirtualHostBuilder WithExcludedExtensions(this IVirtualHostBuilder vhBuilder, IReadOnlySet<string> excludedExtensions) - { - vhBuilder.WithOption(c => c.ExcludedExtensions = excludedExtensions); - return vhBuilder; - } + group.Configs + .SelectMany(static vc => FromVirtualHostConfig(vc) + .Select(vh => new CustomServiceHost<BasicVirtualHost>(vh, vc.UserState) + )) + .ForEach(col.Add); //Force enumeration + }); - public static IVirtualHostBuilder WithAllowedAttributes(this IVirtualHostBuilder vhBuilder, FileAttributes attributes) - { - vhBuilder.WithOption(c => c.AllowedAttributes = attributes); - return vhBuilder; - } - - public static IVirtualHostBuilder WithDisallowedAttributes(this IVirtualHostBuilder vhBuilder, FileAttributes attributes) - { - vhBuilder.WithOption(c => c.DissallowedAttributes = attributes); - return vhBuilder; - } + return this; + } - public static IVirtualHostBuilder WithDownstreamServers(this IVirtualHostBuilder vhBuilder, IReadOnlySet<IPAddress> addresses) - { - vhBuilder.WithOption(c => c.DownStreamServers = addresses); - return vhBuilder; - } + ///<inheritdoc/> + public IDomainBuilder WithHosts(IServiceHost[] hosts) + { + svcBuilder.AddHostCollection(col => Array.ForEach(hosts, col.Add)); + return this; + } - /// <summary> - /// Adds an array of IP addresses to the downstream server collection. This is a security - /// features that allows event handles to trust connections/ipaddresses that originate from - /// trusted downstream servers - /// </summary> - /// <param name="vhBuilder"></param> - /// <param name="addresses">The collection of IP addresses to set as trusted servers</param> - /// <returns></returns> - public static IVirtualHostBuilder WithDownstreamServers(this IVirtualHostBuilder vhBuilder, params IPAddress[] addresses) - { - vhBuilder.WithOption(c => c.DownStreamServers = new HashSet<IPAddress>(addresses)); - return vhBuilder; - } + private static IEnumerable<BasicVirtualHost> FromVirtualHostConfig(VirtualHostConfiguration configuration) + { + /* + * Configurations are allowed to define multiple hostnames for a single + * virtual host. + */ - private static BasicVirtualHost FromVirtualHostConfig(VirtualHostConfiguration configuration) - { - /* - * Event processors configurations are considered immutable. That is, - * top-level elements are not allowed to be changed after the processor - * has been created. Some properties/containers are allowed to be modified - * such as middleware chains, and the service pool. - */ + return configuration.Hostnames + .Select<string, BasicVirtualHost>((string hostname) => + { + /* + * Event processors configurations are considered immutable. That is, + * top-level elements are not allowed to be changed after the processor + * has been created. Some properties/containers are allowed to be modified + * such as middleware chains, and the service pool. + */ + + EventProcessorConfig conf = new( + Directory: configuration.RootDir.FullName, + Hostname: hostname, + Log: configuration.LogProvider, + Options: configuration + ) + { + AllowedAttributes = configuration.AllowedAttributes, + DissallowedAttributes = configuration.DissallowedAttributes, + DefaultFiles = configuration.DefaultFiles, + ExecutionTimeout = configuration.ExecutionTimeout, + FilePathCacheMaxAge = configuration.FilePathCacheMaxAge, + + //Frozen sets are required for the event processor, for performance reasons + DownStreamServers = configuration.DownStreamServers.ToFrozenSet(), + ExcludedExtensions = configuration.ExcludedExtensions.ToFrozenSet(), + }; + + //Add all pre-configured middleware to the chain + configuration.CustomMiddleware.ForEach(conf.MiddlewareChain.Add); + + return new(configuration.EventHooks, conf); + }); + } - EventProcessorConfig conf = new( - configuration.RootDir.FullName, - configuration.Hostname, - configuration.LogProvider, - configuration) + private sealed record class SvGroupBuilder : IServiceGroupBuilder { - AllowedAttributes = configuration.AllowedAttributes, - DissallowedAttributes = configuration.DissallowedAttributes, - DefaultFiles = configuration.DefaultFiles, - ExecutionTimeout = configuration.ExecutionTimeout, + internal readonly List<VirtualHostConfiguration> Configs = new(); - //Frozen sets are required for the event processor, for performance reasons - DownStreamServers = configuration.DownStreamServers.ToFrozenSet(), - ExcludedExtensions = configuration.ExcludedExtensions.ToFrozenSet(), - }; + ///<inheritdoc/> + public IVirtualHostBuilder WithVirtualHost(DirectoryInfo rootDirectory, IVirtualHostHooks hooks, ILogProvider logger) + { + //Create new config instance and add to list + VirtualHostConfiguration config = new() + { + EventHooks = hooks, + RootDir = rootDirectory, + LogProvider = logger + }; + Configs.Add(config); + return new VHostBuilder(config); + } - //Add all pre-configured middleware to the chain - configuration.CustomMiddleware.ForEach(conf.MiddlewareChain.Add); + ///<inheritdoc/> + public IServiceGroupBuilder WithVirtualHost(Action<IVirtualHostBuilder> builder) + { + //Create new config instance and add to list + VirtualHostConfiguration config = new() + { + RootDir = null!, + LogProvider = null! + }; + + //Pass the builder to the callback + builder(new VHostBuilder(config)); + + return WithVirtualHost(config, null); + } - return new(configuration.EventHooks, conf); - } + ///<inheritdoc/> + public IServiceGroupBuilder WithVirtualHost(VirtualHostConfiguration config, object? userState) + { + config.UserState = userState; + Configs.Add(config); + return this; + } + private sealed record class VHostBuilder(VirtualHostConfiguration Config) : IVirtualHostBuilder + { + ///<inheritdoc/> + public IVirtualHostBuilder WithOption(Action<VirtualHostConfiguration> configCallback) + { + configCallback(Config); + return this; + } + } + } + } - private static void AddHosts(this HttpServiceStackBuilder stack, Func<IServiceHost[]> hosts) - => stack.WithDomain(p => Array.ForEach(hosts(), h => p.Add(h))); private static void OnPluginServiceEvent<T>(this IManagedPlugin plugin, Action<T> loader) { @@ -232,48 +201,21 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction } } - private sealed record class DomainBuilder(List<VirtualHostConfiguration> Configs, HttpServiceStackBuilder Stack) : IDomainBuilder - { - ///<inheritdoc/> - public IVirtualHostBuilder WithVirtualHost(DirectoryInfo rootDirectory, IVirtualHostHooks hooks, ILogProvider logger) - { - //Create new config instance and add to list - VirtualHostConfiguration config = new() - { - EventHooks = hooks, - RootDir = rootDirectory, - LogProvider = logger - }; - Configs.Add(config); - return new VHostBuilder(config); - } - ///<inheritdoc/> - public IDomainBuilder WithVirtualHost(VirtualHostConfiguration config) - { - Configs.Add(config); - return this; - } + /* + * The goal of this class is to added the extra service injection + * and manage the IWebRoot instance that will be served by a + * webserver + */ - private sealed record class VHostBuilder(VirtualHostConfiguration Config) : IVirtualHostBuilder - { - ///<inheritdoc/> - public IVirtualHostBuilder WithOption(Action<VirtualHostConfiguration> configCallback) - { - configCallback(Config); - return this; - } - } - } - - private sealed class CustomServiceHost<T>(IHostTransportInfo Config, T Instance) : IServiceHost + private sealed class CustomServiceHost<T>(T Instance, object? userState) : IServiceHost where T : EventProcessor, IRuntimeServiceInjection { ///<inheritdoc/> public IWebRoot Processor => Instance; ///<inheritdoc/> - public IHostTransportInfo TransportInfo => Config; + public object? UserState => userState; ///<inheritdoc/> void IServiceHost.OnRuntimeServiceAttach(IManagedPlugin plugin, IEndpoint[] endpoints) @@ -300,10 +242,12 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction plugin.OnPluginServiceEvent<IEnumerable<IHttpMiddleware>>(p => p.ForEach(Instance.Options.MiddlewareChain.Remove)); plugin.OnPluginServiceEvent<IHttpMiddleware[]>(p => p.ForEach(Instance.Options.MiddlewareChain.Remove)); } + } - private sealed class BasicVirtualHost(IVirtualHostHooks Hooks, EventProcessorConfig config) : EventProcessor(config), IRuntimeServiceInjection + private sealed class BasicVirtualHost(IVirtualHostHooks Hooks, EventProcessorConfig config) + : EventProcessor(config), IRuntimeServiceInjection { /* * Runtime service injection can be tricky, at least in my architecture. If all we have @@ -315,16 +259,20 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction private readonly ConditionalWeakTable<IServiceProvider, Type[]> _exposedTypes = new(); ///<inheritdoc/> - public override bool ErrorHandler(HttpStatusCode errorCode, IHttpEvent entity) => Hooks.ErrorHandler(errorCode, entity); + public override bool ErrorHandler(HttpStatusCode errorCode, IHttpEvent entity) + => Hooks.ErrorHandler(errorCode, entity); ///<inheritdoc/> - public override void PreProcessEntity(HttpEntity entity, out FileProcessArgs preProcArgs) => Hooks.PreProcessEntityAsync(entity, out preProcArgs); + public override void PreProcessEntity(HttpEntity entity, out FileProcessArgs preProcArgs) + => Hooks.PreProcessEntityAsync(entity, out preProcArgs); ///<inheritdoc/> - public override void PostProcessEntity(HttpEntity entity, ref FileProcessArgs chosenRoutine) => Hooks.PostProcessFile(entity, ref chosenRoutine); + public override void PostProcessEntity(HttpEntity entity, ref FileProcessArgs chosenRoutine) + => Hooks.PostProcessFile(entity, ref chosenRoutine); ///<inheritdoc/> - public override string TranslateResourcePath(string requestPath) => Hooks.TranslateResourcePath(requestPath); + public override string TranslateResourcePath(string requestPath) + => Hooks.TranslateResourcePath(requestPath); ///<inheritdoc/> public void AddServices(IServiceProvider services) diff --git a/lib/Plugins.Essentials.ServiceStack/src/Construction/VirtualHostConfiguration.cs b/lib/Plugins.Essentials.ServiceStack/src/Construction/VirtualHostConfiguration.cs index 97ad905..20e346e 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/Construction/VirtualHostConfiguration.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/Construction/VirtualHostConfiguration.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack @@ -26,7 +26,6 @@ using System; using System.IO; using System.Net; using System.Collections.Generic; -using System.Security.Cryptography.X509Certificates; using VNLib.Utils.Logging; using VNLib.Plugins.Essentials.Middleware; @@ -36,34 +35,28 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction /// <summary> /// A virtual host configuration container /// </summary> - public class VirtualHostConfiguration : IHostTransportInfo, IEpProcessingOptions + public class VirtualHostConfiguration : IEpProcessingOptions { /// <summary> - /// The directory that this virtual host will serve files from - /// </summary> - public DirectoryInfo RootDir { get; set; } = null!; - - /// <summary> - /// The hostname, or domain name, that this virtual host will respond to - /// <para>Default: *</para> + /// Optional user state object /// </summary> - public string Hostname { get; set; } = "*"; + internal object? UserState { get; set; } /// <summary> - /// The transport endpoint that this virtual host will listen on - /// <para>Default: 0.0.0.0:80</para> + /// The directory that this virtual host will serve files from /// </summary> - public IPEndPoint TransportEndpoint { get; set; } = new IPEndPoint(IPAddress.Any, 80); + public required DirectoryInfo RootDir { get; set; } /// <summary> - /// An optional certificate to use for TLS connections + /// The hostname, or domain name, that this virtual host will respond to + /// <para>Default: *</para> /// </summary> - public X509Certificate? Certificate { get; set; } + public string[] Hostnames { get; set; } = [ "*" ]; /// <summary> /// A log provider to use for this virtual host /// </summary> - public ILogProvider LogProvider { get; set; } = null!; + public required ILogProvider LogProvider { get; set; } /// <summary> /// The name of a default file to search for within a directory if no file is specified (index.html). @@ -112,6 +105,12 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction /// </summary> public ICollection<IHttpMiddleware> CustomMiddleware { get; } = new List<IHttpMiddleware>(); + /// <summary> + /// A <see cref="TimeSpan"/> for how long a file path may be cached before being revalidated. Setting to + /// zero will disable path caching + /// </summary> + public TimeSpan FilePathCacheMaxAge { get; set; } = TimeSpan.Zero; + internal VirtualHostConfiguration Clone() => (VirtualHostConfiguration)MemberwiseClone(); } } diff --git a/lib/Plugins.Essentials.ServiceStack/src/Construction/VirtualHostConstructionExtensions.cs b/lib/Plugins.Essentials.ServiceStack/src/Construction/VirtualHostConstructionExtensions.cs new file mode 100644 index 0000000..acf1b53 --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/Construction/VirtualHostConstructionExtensions.cs @@ -0,0 +1,107 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.ServiceStack +* File: SsBuilderExtensions.cs +* +* SsBuilderExtensions.cs is part of VNLib.Plugins.Essentials.ServiceStack which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.ServiceStack is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 2 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials.ServiceStack is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Net; +using System.Collections.Generic; + +using VNLib.Utils.Logging; +using VNLib.Plugins.Essentials.Middleware; + +namespace VNLib.Plugins.Essentials.ServiceStack.Construction +{ + public static class VirtualHostConstructionExtensions + { + /// <summary> + /// Adds a single <see cref="IHttpMiddleware"/> instance to the virtual host + /// </summary> + /// <param name="vhBuilder"></param> + /// <param name="middleware">The middleware instance to add</param> + /// <returns></returns> + public static IVirtualHostBuilder WithMiddleware(this IVirtualHostBuilder vhBuilder, IHttpMiddleware middleware) + => vhBuilder.WithOption(c => c.CustomMiddleware.Add(middleware)); + + /// <summary> + /// Adds multiple <see cref="IHttpMiddleware"/> instances to the virtual host + /// </summary> + /// <param name="vhBuilder"></param> + /// <param name="middleware">The array of middleware instances to add to the collection</param> + /// <returns></returns> + public static IVirtualHostBuilder WithMiddleware(this IVirtualHostBuilder vhBuilder, params IHttpMiddleware[] middleware) + => vhBuilder.WithOption(c => Array.ForEach(middleware, m => c.CustomMiddleware.Add(m))); + + + /// <summary> + /// Takes a callback to allow you to inject middelware applications into + /// your virtual host + /// </summary> + /// <param name="vhBuilder"></param> + /// <param name="middleware">The array of middleware instances to add to the collection</param> + /// <returns></returns> + public static IVirtualHostBuilder WithMiddleware(this IVirtualHostBuilder vhBuilder, Action<ICollection<IHttpMiddleware>> middleware) + => vhBuilder.WithOption(c => middleware.Invoke(c.CustomMiddleware)); + + public static IVirtualHostBuilder WithLogger(this IVirtualHostBuilder vhBuilder, ILogProvider logger) + => vhBuilder.WithOption(c => c.LogProvider = logger); + + public static IVirtualHostBuilder WithHostnames(this IVirtualHostBuilder virtualHostBuilder, string[] hostnames) + => virtualHostBuilder.WithOption(c => c.Hostnames = hostnames); + + public static IVirtualHostBuilder WithDefaultFiles(this IVirtualHostBuilder vhBuidler, params string[] defaultFiles) + => vhBuidler.WithDefaultFiles((IReadOnlyCollection<string>)defaultFiles); + + public static IVirtualHostBuilder WithDefaultFiles(this IVirtualHostBuilder vhBuidler, IReadOnlyCollection<string> defaultFiles) + => vhBuidler.WithOption(c => c.DefaultFiles = defaultFiles); + + public static IVirtualHostBuilder WithExcludedExtensions(this IVirtualHostBuilder vhBuilder, params string[] excludedExtensions) + => vhBuilder.WithExcludedExtensions(new HashSet<string>(excludedExtensions)); + + public static IVirtualHostBuilder WithExcludedExtensions(this IVirtualHostBuilder vhBuilder, IReadOnlySet<string> excludedExtensions) + => vhBuilder.WithOption(c => c.ExcludedExtensions = excludedExtensions); + + public static IVirtualHostBuilder WithAllowedAttributes(this IVirtualHostBuilder vhBuilder, FileAttributes attributes) + => vhBuilder.WithOption(c => c.AllowedAttributes = attributes); + + public static IVirtualHostBuilder WithDisallowedAttributes(this IVirtualHostBuilder vhBuilder, FileAttributes attributes) + => vhBuilder.WithOption(c => c.DissallowedAttributes = attributes); + + public static IVirtualHostBuilder WithDownstreamServers(this IVirtualHostBuilder vhBuilder, IReadOnlySet<IPAddress> addresses) + => vhBuilder.WithOption(c => c.DownStreamServers = addresses); + + public static IVirtualHostBuilder WithFilePathCache(this IVirtualHostBuilder vhBuilder, TimeSpan maxAge = default) + => vhBuilder.WithOption(c => c.FilePathCacheMaxAge = maxAge); + + /// <summary> + /// Adds an array of IP addresses to the downstream server collection. This is a security + /// features that allows event handles to trust connections/ipaddresses that originate from + /// trusted downstream servers + /// </summary> + /// <param name="vhBuilder"></param> + /// <param name="addresses">The collection of IP addresses to set as trusted servers</param> + /// <returns></returns> + public static IVirtualHostBuilder WithDownstreamServers(this IVirtualHostBuilder vhBuilder, params IPAddress[] addresses) + => vhBuilder.WithOption(c => c.DownStreamServers = new HashSet<IPAddress>(addresses)); + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs index 71db38f..2f1bb0b 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs @@ -30,6 +30,7 @@ using System.Collections.Generic; using VNLib.Utils; using VNLib.Net.Http; using VNLib.Utils.Logging; +using VNLib.Plugins.Essentials.ServiceStack.Plugins; namespace VNLib.Plugins.Essentials.ServiceStack { @@ -39,7 +40,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// </summary> public sealed class HttpServiceStack : VnDisposeable { - private readonly LinkedList<IHttpServer> _servers; + private readonly IReadOnlyCollection<IHttpServer> _servers; private readonly ServiceDomain _serviceDomain; private readonly PluginManager _plugins; @@ -49,7 +50,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// <summary> /// A collection of all loaded servers /// </summary> - public IReadOnlyCollection<IHttpServer> Servers => _servers; + public IEnumerable<IHttpServer> Servers => _servers; /// <summary> /// Gets the internal <see cref="IHttpPluginManager"/> that manages plugins for the entire @@ -62,7 +63,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// generate servers to listen for services exposed by the /// specified host context /// </summary> - internal HttpServiceStack(LinkedList<IHttpServer> servers, ServiceDomain serviceDomain, IPluginInitializer plugins) + internal HttpServiceStack(IReadOnlyCollection<IHttpServer> servers, ServiceDomain serviceDomain, IPluginInitializer plugins) { _servers = servers; _serviceDomain = serviceDomain; @@ -130,9 +131,6 @@ namespace VNLib.Plugins.Essentials.ServiceStack _cts?.Dispose(); _plugins.Dispose(); - - //remove all lists - _servers.Clear(); } } } diff --git a/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs b/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs index 2517d66..2b5dc64 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack @@ -23,6 +23,7 @@ */ using VNLib.Net.Http; +using VNLib.Plugins.Essentials.ServiceStack.Plugins; namespace VNLib.Plugins.Essentials.ServiceStack { @@ -40,9 +41,9 @@ namespace VNLib.Plugins.Essentials.ServiceStack IWebRoot Processor { get; } /// <summary> - /// The host's transport information + /// Optional user state to be set during initialization and read at a later time /// </summary> - IHostTransportInfo TransportInfo { get; } + object? UserState { get; } /// <summary> /// Called when a plugin is loaded and is endpoints are extracted diff --git a/lib/Plugins.Essentials.ServiceStack/src/IHttpPluginManager.cs b/lib/Plugins.Essentials.ServiceStack/src/Plugins/IHttpPluginManager.cs index 6cece1f..23e436b 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/IHttpPluginManager.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/Plugins/IHttpPluginManager.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack @@ -25,7 +25,7 @@ using System; using System.Collections.Generic; -namespace VNLib.Plugins.Essentials.ServiceStack +namespace VNLib.Plugins.Essentials.ServiceStack.Plugins { /// <summary> /// Represents a live plugin controller that manages all diff --git a/lib/Plugins.Essentials.ServiceStack/src/IManagedPlugin.cs b/lib/Plugins.Essentials.ServiceStack/src/Plugins/IManagedPlugin.cs index 8332f7e..b2506c5 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/IManagedPlugin.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/Plugins/IManagedPlugin.cs @@ -25,7 +25,7 @@ using System; using System.ComponentModel.Design; -namespace VNLib.Plugins.Essentials.ServiceStack +namespace VNLib.Plugins.Essentials.ServiceStack.Plugins { /// <summary> /// Represents a plugin managed by a <see cref="IHttpPluginManager"/> that includes dynamically loaded plugins diff --git a/lib/Plugins.Essentials.ServiceStack/src/IManualPlugin.cs b/lib/Plugins.Essentials.ServiceStack/src/Plugins/IManualPlugin.cs index cecd481..57fd631 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/IManualPlugin.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/Plugins/IManualPlugin.cs @@ -25,7 +25,7 @@ using System; using System.ComponentModel.Design; -namespace VNLib.Plugins.Essentials.ServiceStack +namespace VNLib.Plugins.Essentials.ServiceStack.Plugins { /// <summary> /// Represents a plugin that may be added to a service stack in user-code diff --git a/lib/Plugins.Essentials.ServiceStack/src/IPluginInitializer.cs b/lib/Plugins.Essentials.ServiceStack/src/Plugins/IPluginInitializer.cs index a9aa103..9435d9b 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/IPluginInitializer.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/Plugins/IPluginInitializer.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack @@ -25,7 +25,7 @@ using VNLib.Utils.Logging; -namespace VNLib.Plugins.Essentials.ServiceStack +namespace VNLib.Plugins.Essentials.ServiceStack.Plugins { internal interface IPluginInitializer { diff --git a/lib/Plugins.Essentials.ServiceStack/src/PluginExtensions.cs b/lib/Plugins.Essentials.ServiceStack/src/Plugins/PluginExtensions.cs index 52377ee..4d04c54 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/PluginExtensions.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/Plugins/PluginExtensions.cs @@ -27,7 +27,7 @@ using System.Collections.Generic; using VNLib.Plugins.Essentials.Runtime; -namespace VNLib.Plugins.Essentials.ServiceStack +namespace VNLib.Plugins.Essentials.ServiceStack.Plugins { /// <summary> /// Internal and service stack specific extensions for plugins diff --git a/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs b/lib/Plugins.Essentials.ServiceStack/src/Plugins/PluginManager.cs index f43da78..ce80a9e 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/Plugins/PluginManager.cs @@ -29,7 +29,7 @@ using System.Collections.Generic; using VNLib.Utils; using VNLib.Utils.Logging; -namespace VNLib.Plugins.Essentials.ServiceStack +namespace VNLib.Plugins.Essentials.ServiceStack.Plugins { /// <summary> @@ -70,9 +70,9 @@ namespace VNLib.Plugins.Essentials.ServiceStack { Check(); - foreach(IManagedPlugin plugin in _loadedPlugins) + foreach (IManagedPlugin plugin in _loadedPlugins) { - if(plugin.SendCommandToPlugin(pluginName, message, nameComparison)) + if (plugin.SendCommandToPlugin(pluginName, message, nameComparison)) { return true; } diff --git a/lib/Plugins.Essentials.ServiceStack/src/PluginRutimeEventHandler.cs b/lib/Plugins.Essentials.ServiceStack/src/Plugins/PluginRutimeEventHandler.cs index f892823..c5b094f 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/PluginRutimeEventHandler.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/Plugins/PluginRutimeEventHandler.cs @@ -28,7 +28,7 @@ using System.Linq; using VNLib.Utils.Extensions; using VNLib.Plugins.Runtime; -namespace VNLib.Plugins.Essentials.ServiceStack +namespace VNLib.Plugins.Essentials.ServiceStack.Plugins { internal sealed class PluginRutimeEventHandler(ServiceDomain Domain) : IPluginEventListener { diff --git a/lib/Plugins.Essentials.ServiceStack/src/PluginStackInitializer.cs b/lib/Plugins.Essentials.ServiceStack/src/Plugins/PluginStackInitializer.cs index 5f4e6e0..fb685f7 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/PluginStackInitializer.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/Plugins/PluginStackInitializer.cs @@ -36,15 +36,15 @@ using VNLib.Plugins.Runtime; using VNLib.Utils.Extensions; using VNLib.Plugins.Runtime.Services; -namespace VNLib.Plugins.Essentials.ServiceStack +namespace VNLib.Plugins.Essentials.ServiceStack.Plugins { - internal sealed class PluginStackInitializer(PluginRutimeEventHandler Listener, IPluginStack Stack, IManualPlugin[] ManualPlugins, bool ConcurrentLoad) + internal sealed class PluginStackInitializer(PluginRutimeEventHandler Listener, IPluginStack Stack, IManualPlugin[] ManualPlugins, bool ConcurrentLoad) : IPluginInitializer { private readonly LinkedList<IManagedPlugin> _managedPlugins = new(); private readonly LinkedList<ManualPluginWrapper> _manualPlugins = new(); - + private void PrepareStack() { /* @@ -79,7 +79,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack //Combine all managed plugins and initialize them individually IEnumerable<IManagedPlugin> plugins = _managedPlugins.Union(_manualPlugins); - foreach(IManagedPlugin p in plugins) + foreach (IManagedPlugin p in plugins) { //Try init plugin and add it to the list of loaded plugins if (InitializePluginCore(p, debugLog)) @@ -153,7 +153,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack debugLog.Warn("No plugin instances were exposed via {asm} assembly. This may be due to an assebmly mismatch", plugin.ToString()); } } - else if(plugin is ManualPluginWrapper mpw) + else if (plugin is ManualPluginWrapper mpw) { //Initialzie plugin wrapper mpw.Plugin.Initialize(); @@ -312,8 +312,8 @@ namespace VNLib.Plugins.Essentials.ServiceStack return false; } - - void IManagedPlugin.OnPluginLoaded() + + void IManagedPlugin.OnPluginLoaded() { } void IManagedPlugin.OnPluginUnloaded() diff --git a/lib/Plugins.Essentials.ServiceStack/src/ServiceDomain.cs b/lib/Plugins.Essentials.ServiceStack/src/ServiceDomain.cs index 35bf65b..52e4984 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/ServiceDomain.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/ServiceDomain.cs @@ -23,11 +23,10 @@ */ using System; -using System.Net; -using System.Linq; using System.Collections.Generic; using VNLib.Utils.Extensions; +using VNLib.Plugins.Essentials.ServiceStack.Construction; namespace VNLib.Plugins.Essentials.ServiceStack { @@ -38,7 +37,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// </summary> public sealed class ServiceDomain { - private readonly LinkedList<ServiceGroup> _serviceGroups = new(); + private ServiceGroup[] _serviceGroups = []; /// <summary> /// Gets all service groups loaded in the service manager @@ -51,15 +50,11 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// </summary> /// <param name="hostBuilder">The callback method to build virtual hosts</param> /// <returns>A value that indicates if any virtual hosts were successfully loaded</returns> - public bool BuildDomain(Action<ICollection<IServiceHost>> hostBuilder) + public void BuildDomain(ServiceBuilder hostBuilder) { - //LL to store created hosts - LinkedList<IServiceHost> hosts = new(); + ArgumentNullException.ThrowIfNull(hostBuilder); - //build hosts - hostBuilder.Invoke(hosts); - - return FromExisting(hosts); + FromExisting(hostBuilder.BuildGroups()); } /// <summary> @@ -67,36 +62,11 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// </summary> /// <param name="hosts">The enumeration of virtual hosts</param> /// <returns>A value that indicates if any virtual hosts were successfully loaded</returns> - public bool FromExisting(IEnumerable<IServiceHost> hosts) - { - //Get service groups and pass service group list - CreateServiceGroups(_serviceGroups, hosts); - return _serviceGroups.Any(); - } - - private static void CreateServiceGroups(ICollection<ServiceGroup> groups, IEnumerable<IServiceHost> hosts) + public void FromExisting(IEnumerable<ServiceGroup> hosts) { - //Get distinct interfaces - IPEndPoint[] interfaces = hosts.Select(static s => s.TransportInfo.TransportEndpoint).Distinct().ToArray(); - - //Select hosts of the same interface to create a group from - foreach (IPEndPoint iface in interfaces) - { - IEnumerable<IServiceHost> groupHosts = hosts.Where(host => host.TransportInfo.TransportEndpoint.Equals(iface)); + ArgumentNullException.ThrowIfNull(hosts); - //Find any duplicate hostnames for the same service gorup - IServiceHost[] overlap = groupHosts.Where(vh => groupHosts.Select(static s => s.Processor.Hostname).Count(hostname => vh.Processor.Hostname == hostname) > 1).ToArray(); - - if(overlap.Length > 0) - { - throw new ArgumentException($"The hostname '{overlap.Last().Processor.Hostname}' is already in use by another virtual host"); - } - - //init new service group around an interface and its roots - ServiceGroup group = new(iface, groupHosts); - - groups.Add(group); - } + hosts.ForEach(h => _serviceGroups = [.. _serviceGroups, h]); } /// <summary> @@ -106,9 +76,11 @@ namespace VNLib.Plugins.Essentials.ServiceStack public void TearDown() { //Manually cleanup if unload missed data - _serviceGroups.TryForeach(static sg => sg.UnloadAll()); - //empty service groups - _serviceGroups.Clear(); + Array.ForEach(_serviceGroups, static sg => sg.UnloadAll()); + + Array.Clear(_serviceGroups); + + _serviceGroups = []; } } } diff --git a/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs b/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs index 29b9fdc..a446e37 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs @@ -28,6 +28,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using VNLib.Utils.Extensions; +using VNLib.Plugins.Essentials.ServiceStack.Plugins; namespace VNLib.Plugins.Essentials.ServiceStack { @@ -41,19 +42,13 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// Initalizes a new <see cref="ServiceGroup"/> of virtual hosts /// with common transport /// </remarks> - /// <param name="serviceEndpoint">The <see cref="IPEndPoint"/> to listen for connections on</param> /// <param name="hosts">The hosts that share a common interface endpoint</param> - public sealed class ServiceGroup(IPEndPoint serviceEndpoint, IEnumerable<IServiceHost> hosts) + public sealed class ServiceGroup(IEnumerable<IServiceHost> hosts) { private readonly LinkedList<IServiceHost> _vHosts = new(hosts); private readonly ConditionalWeakTable<IManagedPlugin, IEndpoint[]> _endpointsForPlugins = new(); /// <summary> - /// The <see cref="IPEndPoint"/> transport endpoint for all loaded service hosts - /// </summary> - public IPEndPoint ServiceEndpoint => serviceEndpoint; - - /// <summary> /// The collection of hosts that are loaded by this group /// </summary> public IReadOnlyCollection<IServiceHost> Hosts => _vHosts; |