aboutsummaryrefslogtreecommitdiff
path: root/apps/VNLib.WebServer/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/VNLib.WebServer/src')
-rw-r--r--apps/VNLib.WebServer/src/Bootstrap/ReleaseWebserver.cs333
-rw-r--r--apps/VNLib.WebServer/src/Bootstrap/VariableLogFormatter.cs57
-rw-r--r--apps/VNLib.WebServer/src/Bootstrap/WebserverBase.cs203
-rw-r--r--apps/VNLib.WebServer/src/CommandListener.cs251
-rw-r--r--apps/VNLib.WebServer/src/Compression/FallbackCompressionManager.cs144
-rw-r--r--apps/VNLib.WebServer/src/Compression/HttpCompressor.cs123
-rw-r--r--apps/VNLib.WebServer/src/Config/IServerConfig.cs34
-rw-r--r--apps/VNLib.WebServer/src/Config/JsonConfigOptions.cs42
-rw-r--r--apps/VNLib.WebServer/src/Config/JsonServerConfig.cs156
-rw-r--r--apps/VNLib.WebServer/src/Config/Model/BenchmarkConfig.cs42
-rw-r--r--apps/VNLib.WebServer/src/Config/Model/CorsSecurityConfig.cs41
-rw-r--r--apps/VNLib.WebServer/src/Config/Model/ErrorFileConfig.cs37
-rw-r--r--apps/VNLib.WebServer/src/Config/Model/HttpCompressorConfig.cs47
-rw-r--r--apps/VNLib.WebServer/src/Config/Model/HttpGlobalConfig.cs137
-rw-r--r--apps/VNLib.WebServer/src/Config/Model/ServerPluginConfig.cs46
-rw-r--r--apps/VNLib.WebServer/src/Config/Model/TcpConfigJson.cs73
-rw-r--r--apps/VNLib.WebServer/src/Config/Model/TransportInterface.cs137
-rw-r--r--apps/VNLib.WebServer/src/Config/Model/VirtualHostServerConfig.cs96
-rw-r--r--apps/VNLib.WebServer/src/Config/ServerConfigurationException.cs40
-rw-r--r--apps/VNLib.WebServer/src/Config/Validate.cs113
-rw-r--r--apps/VNLib.WebServer/src/Entry.cs336
-rw-r--r--apps/VNLib.WebServer/src/MemoryPoolManager.cs137
-rw-r--r--apps/VNLib.WebServer/src/Middlewares/BenchmarkMiddleware.cs102
-rw-r--r--apps/VNLib.WebServer/src/Middlewares/CORSMiddleware.cs182
-rw-r--r--apps/VNLib.WebServer/src/Middlewares/ConnectionLogMiddleware.cs108
-rw-r--r--apps/VNLib.WebServer/src/Middlewares/IpBlacklistMiddleware.cs49
-rw-r--r--apps/VNLib.WebServer/src/Middlewares/IpWhitelistMiddleware.cs53
-rw-r--r--apps/VNLib.WebServer/src/Middlewares/MainServerMiddlware.cs100
-rw-r--r--apps/VNLib.WebServer/src/Plugins/PluginAssemblyLoader.cs125
-rw-r--r--apps/VNLib.WebServer/src/RuntimeLoading/ProcessArguments.cs40
-rw-r--r--apps/VNLib.WebServer/src/RuntimeLoading/ServerLogBuilder.cs154
-rw-r--r--apps/VNLib.WebServer/src/RuntimeLoading/ServerLogger.cs45
-rw-r--r--apps/VNLib.WebServer/src/Transport/HostAwareServerSslOptions.cs108
-rw-r--r--apps/VNLib.WebServer/src/Transport/SslTcpTransportContext.cs103
-rw-r--r--apps/VNLib.WebServer/src/Transport/TcpServerLoader.cs221
-rw-r--r--apps/VNLib.WebServer/src/Transport/TcpTransport.cs188
-rw-r--r--apps/VNLib.WebServer/src/Transport/TcpTransportContext.cs93
-rw-r--r--apps/VNLib.WebServer/src/VLogProvider.cs84
-rw-r--r--apps/VNLib.WebServer/src/VNLib.WebServer.csproj88
-rw-r--r--apps/VNLib.WebServer/src/VirtualHosts/FileCache.cs134
-rw-r--r--apps/VNLib.WebServer/src/VirtualHosts/IVirtualHostConfigBuilder.cs35
-rw-r--r--apps/VNLib.WebServer/src/VirtualHosts/JsonWebConfigBuilder.cs276
-rw-r--r--apps/VNLib.WebServer/src/VirtualHosts/SpecialHeaders.cs71
-rw-r--r--apps/VNLib.WebServer/src/VirtualHosts/VirtualHostConfig.cs101
-rw-r--r--apps/VNLib.WebServer/src/VirtualHosts/VirtualHostHooks.cs231
-rw-r--r--apps/VNLib.WebServer/src/sample.config.json167
46 files changed, 5483 insertions, 0 deletions
diff --git a/apps/VNLib.WebServer/src/Bootstrap/ReleaseWebserver.cs b/apps/VNLib.WebServer/src/Bootstrap/ReleaseWebserver.cs
new file mode 100644
index 0000000..e7834bb
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Bootstrap/ReleaseWebserver.cs
@@ -0,0 +1,333 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: ReleaseWebserver.cs
+*
+* ReleaseWebserver.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Collections.Generic;
+
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Net.Http;
+using VNLib.Plugins.Runtime;
+
+using VNLib.WebServer.Config;
+using VNLib.WebServer.Config.Model;
+using VNLib.WebServer.Plugins;
+using VNLib.WebServer.Compression;
+using VNLib.WebServer.Middlewares;
+using VNLib.WebServer.RuntimeLoading;
+using static VNLib.WebServer.Entry;
+
+namespace VNLib.WebServer.Bootstrap
+{
+
+ /*
+ * This class represents a normally loaded "Relase" webserver to allow
+ * for module webserver use-cases. It relies on a system configuration
+ * file and command line arguments to configure the server.
+ */
+
+ internal class ReleaseWebserver(ServerLogger logger, IServerConfig config, ProcessArguments procArgs)
+ : WebserverBase(logger, config, procArgs)
+ {
+
+ const string PLUGIN_DATA_TEMPLATE =
+@"
+----------------------------------
+ | Plugin configuration:
+ | Enabled: {enabled}
+ | Directory: {dir}
+ | Hot Reload: {hr}
+ | Reload Delay: {delay}s
+----------------------------------";
+
+ private readonly ProcessArguments args = procArgs;
+
+ ///<inheritdoc/>
+ protected override PluginStackBuilder? ConfigurePlugins()
+ {
+ //do not load plugins if disabled
+ if (args.HasArgument("--no-plugins"))
+ {
+ logger.AppLog.Information("Plugin loading disabled via command-line flag");
+ return null;
+ }
+
+ JsonElement confEl = config.GetDocumentRoot();
+
+ if (!confEl.TryGetProperty(PLUGINS_CONFIG_PROP_NAME, out JsonElement plCfg))
+ {
+ logger.AppLog.Debug("No plugin configuration found");
+ return null;
+ }
+
+ ServerPluginConfig? conf = plCfg.DeserializeElement<ServerPluginConfig>();
+ Validate.EnsureNotNull(conf, "Your plugin configuration object is null or malformatted");
+
+ if (!conf.Enabled)
+ {
+ logger.AppLog.Information("Plugin loading disabled via configuration flag");
+ return null;
+ }
+
+ Validate.EnsureNotNull(conf.Path, "If plugins are enabled, you must specify a directory to load them from");
+
+ //Init new plugin stack builder
+ PluginStackBuilder pluginBuilder = PluginStackBuilder.Create()
+ .WithDebugLog(logger.AppLog)
+ .WithSearchDirectories([ conf.Path ])
+ .WithLoaderFactory(PluginAsemblyLoading.Create);
+
+ //Setup plugin config data
+ if (!string.IsNullOrWhiteSpace(conf.ConfigDir))
+ {
+ pluginBuilder.WithJsonConfigDir(confEl, new(conf.ConfigDir));
+ }
+ else
+ {
+ pluginBuilder.WithLocalJsonConfig(confEl);
+ }
+
+ if (conf.HotReload)
+ {
+ Validate.EnsureRange(conf.ReloadDelaySec, 1, 120);
+
+ pluginBuilder.EnableHotReload(TimeSpan.FromSeconds(conf.ReloadDelaySec));
+ }
+
+ logger.AppLog.Information(
+ PLUGIN_DATA_TEMPLATE,
+ true,
+ conf.Path,
+ conf.HotReload,
+ conf.ReloadDelaySec
+ );
+
+ if (conf.HotReload)
+ {
+ logger.AppLog.Warn("Plugin hot-reload is not recommended for production deployments!");
+ }
+
+ return pluginBuilder;
+ }
+
+ ///<inheritdoc/>
+ protected override HttpConfig GetHttpConfig()
+ {
+ JsonElement rootEl = config.GetDocumentRoot();
+
+ try
+ {
+ HttpGlobalConfig? gConf = rootEl.GetProperty("http").DeserializeElement<HttpGlobalConfig>();
+ Validate.EnsureNotNull(gConf, "Missing required HTTP configuration variables");
+
+ gConf.ValidateConfig();
+
+ //Attempt to load the compressor manager, if null, compression is disabled
+ IHttpCompressorManager? compressorManager = HttpCompressor.LoadOrDefaultCompressor(procArgs, gConf.Compression, config, logger.AppLog);
+
+ IHttpMemoryPool memPool = MemoryPoolManager.GetHttpPool(procArgs.ZeroAllocations);
+
+ HttpConfig conf = new(Encoding.ASCII)
+ {
+ ActiveConnectionRecvTimeout = gConf.RecvTimeoutMs,
+ CompressorManager = compressorManager,
+ ConnectionKeepAlive = TimeSpan.FromMilliseconds(gConf.KeepAliveMs),
+ CompressionLimit = gConf.Compression.CompressionMax,
+ CompressionMinimum = gConf.Compression.CompressionMin,
+ DebugPerformanceCounters = procArgs.HasArgument("--http-counters"),
+ DefaultHttpVersion = HttpHelpers.ParseHttpVersion(gConf.DefaultHttpVersion),
+ MaxFormDataUploadSize = gConf.MultipartMaxSize,
+ MaxUploadSize = gConf.MaxEntitySize,
+ MaxRequestHeaderCount = gConf.MaxRequestHeaderCount,
+ MaxOpenConnections = gConf.MaxConnections,
+ MaxUploadsPerRequest = gConf.MaxUploadsPerRequest,
+ SendTimeout = gConf.SendTimeoutMs,
+ ServerLog = logger.AppLog,
+ MemoryPool = memPool,
+
+ RequestDebugLog = procArgs.LogHttp ? logger.AppLog : null,
+
+ //Buffer config update
+ BufferConfig = new()
+ {
+ RequestHeaderBufferSize = gConf.HeaderBufSize,
+ ResponseHeaderBufferSize = gConf.ResponseHeaderBufSize,
+ FormDataBufferSize = gConf.MultipartMaxBufSize,
+
+ //Align response buffer size with transport buffer to avoid excessive copy
+ ResponseBufferSize = TcpConfig.TcpTxBufferSize,
+
+ /*
+ * Chunk buffers are written to the transport when they are fully accumulated. These buffers
+ * should be aligned with the transport sizes. It should also be large enough not to put too much
+ * back pressure on compressors. This buffer will be segmented into smaller buffers if it has to
+ * at the transport level, but we should avoid that if possible due to multiple allocations and
+ * copies.
+ *
+ * Aligning chunk buffer to the transport buffer size is the easiest solution to avoid excessive
+ * copyies
+ */
+ ChunkedResponseAccumulatorSize = compressorManager != null ? TcpConfig.TcpTxBufferSize : 0
+ },
+
+ };
+
+ Validate.Assert(
+ condition: conf.DefaultHttpVersion != HttpVersion.None,
+ message: "Your default HTTP version is invalid, specify an RFC formatted http version 'HTTP/x.x'"
+ );
+
+ return conf;
+ }
+ catch (KeyNotFoundException kne)
+ {
+ logger.AppLog.Error("Missing required HTTP configuration variables {var}", kne.Message);
+ throw new ServerConfigurationException("Missing required http variables. Cannot continue");
+ }
+ }
+
+ ///<inheritdoc/>
+ protected override VirtualHostConfig[] GetAllVirtualHosts()
+ {
+ JsonElement rootEl = config.GetDocumentRoot();
+ ILogProvider log = logger.AppLog;
+
+ LinkedList<VirtualHostConfig> configs = new();
+
+ try
+ {
+ //execution timeout
+ TimeSpan execTimeout = rootEl.GetProperty(SESSION_TIMEOUT_PROP_NAME).GetTimeSpan(TimeParseType.Milliseconds);
+
+ int index = 0;
+
+ //Enumerate all virtual host configurations
+ foreach (VirtualHostServerConfig vhConfig in GetVirtualHosts())
+ {
+
+ VirtualHostConfig conf = new JsonWebConfigBuilder(vhConfig, execTimeout, log).GetBaseConfig();
+
+ //Configure event hooks
+ conf.EventHooks = new VirtualHostHooks(conf);
+
+ //Init middleware stack
+ conf.CustomMiddleware.Add(new MainServerMiddlware(log, conf, vhConfig.ForcePortCheck));
+
+ /*
+ * In benchmark mode, skip other middleware that might slow connections down
+ */
+ if (vhConfig.Benchmark?.Enabled == true)
+ {
+ conf.CustomMiddleware.Add(new BenchmarkMiddleware(vhConfig.Benchmark));
+ log.Information("BENCHMARK: Enabled for virtual host {vh}", conf.Hostnames);
+ }
+ else
+ {
+ /*
+ * We only enable cors if the configuration has a value for the allow cors property.
+ * The user may disable cors totally, deny cors requests, or enable cors with a whitelist
+ *
+ * Only add the middleware if the confg has a value for the allow cors property
+ */
+ if (vhConfig.Cors?.Enabled == true)
+ {
+ conf.CustomMiddleware.Add(new CORSMiddleware(log, vhConfig.Cors));
+ }
+
+ //Add whitelist middleware if the configuration has a whitelist
+ if (conf.WhiteList != null)
+ {
+ conf.CustomMiddleware.Add(new IpWhitelistMiddleware(log, conf.WhiteList));
+ }
+
+ //Add blacklist middleware if the configuration has a blacklist
+ if (conf.BlackList != null)
+ {
+ conf.CustomMiddleware.Add(new IpBlacklistMiddleware(log, conf.BlackList));
+ }
+
+ //Add tracing middleware if enabled
+ if (vhConfig.RequestTrace)
+ {
+ conf.CustomMiddleware.Add(new ConnectionLogMiddleware(log));
+ }
+ }
+
+ if (!conf.RootDir.Exists)
+ {
+ conf.RootDir.Create();
+ }
+
+ configs.AddLast(conf);
+
+ index++;
+ }
+ }
+ catch (KeyNotFoundException kne)
+ {
+ throw new ServerConfigurationException("Missing required configuration varaibles", kne);
+ }
+ catch (FormatException fe)
+ {
+ throw new ServerConfigurationException("Failed to parse IP address", fe);
+ }
+
+ return configs.ToArray();
+ }
+
+ private VirtualHostServerConfig[] GetVirtualHosts()
+ {
+ JsonElement rootEl = config.GetDocumentRoot();
+ ILogProvider log = logger.AppLog;
+
+ if (!rootEl.TryGetProperty("virtual_hosts", out _))
+ {
+ log.Warn("No virtual hosts array was defined. Continuing without hosts");
+ return [];
+ }
+
+ return rootEl.GetProperty("virtual_hosts")
+ .EnumerateArray()
+ .Select(GetVhConfig)
+ .ToArray();
+
+
+ static VirtualHostServerConfig GetVhConfig(JsonElement rootEl)
+ {
+ VirtualHostServerConfig? conf = rootEl.DeserializeElement<VirtualHostServerConfig>();
+
+ Validate.EnsureNotNull(conf, "Empty virtual host configuration, check your virtual hosts array for an empty element");
+ Validate.EnsureNotNull(conf.DirPath, "A virtual host was defined without a root directory property: 'dirPath'");
+ Validate.EnsureNotNull(conf.Hostnames, "A virtual host was defined without a hostname property: 'hostnames'");
+ Validate.EnsureNotNull(conf.Interfaces, "An interface configuration is required for every virtual host");
+
+ return conf;
+ }
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Bootstrap/VariableLogFormatter.cs b/apps/VNLib.WebServer/src/Bootstrap/VariableLogFormatter.cs
new file mode 100644
index 0000000..45d7bbb
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Bootstrap/VariableLogFormatter.cs
@@ -0,0 +1,57 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: VariableLogFormatter.cs
+*
+* VariableLogFormatter.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Text;
+using System.Collections.Generic;
+
+using VNLib.Utils.Logging;
+
+namespace VNLib.WebServer.Bootstrap
+{
+ internal sealed class VariableLogFormatter(ILogProvider logger, LogLevel level)
+ {
+ private readonly StringBuilder _logFormatSb = new();
+ private readonly List<object?> _formatArgs = [];
+
+ public void AppendLine(string line) => _logFormatSb.AppendLine(line);
+
+ public void Append(string value) => _logFormatSb.Append(value);
+
+ public void AppendFormat(string format, params object?[] formatargs)
+ {
+ _logFormatSb.Append(format);
+ _formatArgs.AddRange(formatargs);
+ }
+
+ public void AppendLine() => _logFormatSb.AppendLine();
+
+ public void Flush()
+ {
+ logger.Write(level, _logFormatSb.ToString(), [.._formatArgs]);
+
+ _logFormatSb.Clear();
+ _formatArgs.Clear();
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Bootstrap/WebserverBase.cs b/apps/VNLib.WebServer/src/Bootstrap/WebserverBase.cs
new file mode 100644
index 0000000..f3832c6
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Bootstrap/WebserverBase.cs
@@ -0,0 +1,203 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: WebserverBase.cs
+*
+* WebserverBase.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Text.Json;
+using System.Diagnostics;
+
+using VNLib.Net.Http;
+using VNLib.Utils;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins.Runtime;
+using VNLib.Plugins.Essentials.ServiceStack;
+using VNLib.Plugins.Essentials.ServiceStack.Construction;
+
+using VNLib.WebServer.Config;
+using VNLib.WebServer.Transport;
+using VNLib.WebServer.RuntimeLoading;
+
+namespace VNLib.WebServer.Bootstrap
+{
+
+ internal abstract class WebserverBase(ServerLogger logger, IServerConfig config, ProcessArguments procArgs)
+ : VnDisposeable
+ {
+
+ protected readonly ProcessArguments procArgs = procArgs;
+ protected readonly IServerConfig config = config;
+ protected readonly ServerLogger logger = logger;
+ protected readonly TcpServerLoader TcpConfig = new(config, procArgs, logger.SysLog);
+
+ private HttpServiceStack? _serviceStack;
+
+ /// <summary>
+ /// Gets the internal <see cref="HttpServiceStack"/> this
+ /// controller is managing
+ /// </summary>
+ public HttpServiceStack ServiceStack
+ {
+ get
+ {
+ if (_serviceStack is null)
+ {
+ throw new InvalidOperationException("Service stack has not been configured yet");
+ }
+
+ return _serviceStack;
+ }
+ }
+
+ /// <summary>
+ /// Configures the http server for the application so
+ /// its ready to start
+ /// </summary>
+ public virtual void Configure()
+ {
+ _serviceStack = ConfiugreServiceStack();
+ }
+
+ protected virtual HttpServiceStack ConfiugreServiceStack()
+ {
+ bool loadPluginsConcurrently = !procArgs.HasArgument("--sequential-load");
+
+ JsonElement conf = config.GetDocumentRoot();
+
+ HttpConfig http = GetHttpConfig();
+
+ VirtualHostConfig[] virtualHosts = GetAllVirtualHosts();
+
+ PluginStackBuilder? plugins = ConfigurePlugins();
+
+ HttpServiceStackBuilder builder = new HttpServiceStackBuilder()
+ .LoadPluginsConcurrently(loadPluginsConcurrently)
+ .WithBuiltInHttp(TcpConfig.ReduceBindingsForGroups, http)
+ .WithDomain(domain =>
+ {
+ domain.WithServiceGroups(vh =>
+ {
+ /*
+ * Must pass the virtual host configuration as the state object
+ * so transport providers can be loaded from a given virtual host
+ */
+ virtualHosts.ForEach(vhConfig => vh.WithVirtualHost(vhConfig, vhConfig));
+ });
+ });
+
+ if (plugins != null)
+ {
+ builder.WithPluginStack(plugins.ConfigureStack);
+ }
+
+ PrintLogicalRouting(virtualHosts);
+
+ return builder.Build();
+ }
+
+ protected abstract VirtualHostConfig[] GetAllVirtualHosts();
+
+ protected abstract HttpConfig GetHttpConfig();
+
+ protected abstract PluginStackBuilder? ConfigurePlugins();
+
+ /// <summary>
+ /// Starts the server and returns immediately
+ /// after server start listening
+ /// </summary>
+ public void Start()
+ {
+ /* Since this api is uses internally, knowing the order of operations is a bug, not a rumtime accident */
+ Debug.Assert(Disposed == false, "Server was disposed");
+ Debug.Assert(_serviceStack != null, "Server was not configured");
+
+ //Attempt to load plugins before starting server
+ _serviceStack.LoadPlugins(logger.AppLog);
+
+ _serviceStack.StartServers();
+ }
+
+ /// <summary>
+ /// Stops the server and waits for all connections to close and
+ /// servers to fully shut down
+ /// </summary>
+ public void Stop()
+ {
+ Debug.Assert(Disposed == false, "Server was disposed");
+ Debug.Assert(_serviceStack != null, "Server was not configured");
+
+ //Stop the server and wait synchronously
+ _serviceStack.StopAndWaitAsync()
+ .GetAwaiter()
+ .GetResult();
+ }
+
+ private void PrintLogicalRouting(VirtualHostConfig[] hosts)
+ {
+ const string header =@"
+===================================================
+ --- HTTP Service Domain ---
+
+ {enabledRoutes} routes enabled
+";
+
+ VariableLogFormatter sb = new(logger.AppLog, Utils.Logging.LogLevel.Information);
+ sb.AppendFormat(header, hosts.Length);
+
+ foreach (VirtualHostConfig host in hosts)
+ {
+ sb.AppendLine();
+
+ sb.AppendFormat("Virtual Host: {hostnames}\n", (object)host.Hostnames);
+ sb.AppendFormat(" Root directory {rdir}\n", host.RootDir);
+ sb.AppendLine();
+
+ //Print interfaces
+
+ string[] interfaces = host.Transports
+ .Select(i =>$" - {i.Address}:{i.Port} TLS: {i.Ssl}, Client cert: {i.ClientCertRequired}, OS Ciphers: {i.UseOsCiphers}")
+ .ToArray();
+
+ sb.AppendLine(" Interfaces:");
+ sb.AppendFormat("{interfaces}", string.Join("\n", interfaces));
+ sb.AppendLine();
+
+ sb.AppendLine(" Options:");
+ sb.AppendFormat(" - Whitelist: {wl}\n", host.WhiteList);
+ sb.AppendFormat(" - Blacklist: {bl}\n", host.BlackList);
+ sb.AppendFormat(" - Path filter: {filter}\n", host.PathFilter);
+ sb.AppendFormat(" - Cache default time: {cache}\n", host.CacheDefault);
+ sb.AppendFormat(" - Cached error files: {files}\n", host.FailureFiles.Select(static p => (int)p.Key));
+ sb.AppendFormat(" - Downstream servers: {dsServers}\n", host.DownStreamServers);
+ sb.AppendFormat(" - Middlewares loaded {mw}\n", host.CustomMiddleware.Count);
+ sb.AppendLine();
+
+ sb.Flush();
+ }
+ }
+
+
+ ///<inheritdoc/>
+ protected override void Free() => _serviceStack?.Dispose();
+ }
+}
diff --git a/apps/VNLib.WebServer/src/CommandListener.cs b/apps/VNLib.WebServer/src/CommandListener.cs
new file mode 100644
index 0000000..7082d40
--- /dev/null
+++ b/apps/VNLib.WebServer/src/CommandListener.cs
@@ -0,0 +1,251 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: CommandListener.cs
+*
+* CommandListener.cs is part of VNLib.WebServer which is part of
+* the larger VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Threading;
+
+using VNLib.Utils.Memory;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Utils.Memory.Diagnostics;
+using VNLib.Net.Http;
+using VNLib.Plugins.Essentials.ServiceStack;
+using VNLib.Plugins.Essentials.ServiceStack.Plugins;
+
+using VNLib.WebServer.Bootstrap;
+
+namespace VNLib.WebServer
+{
+
+ internal sealed class CommandListener(ManualResetEvent shutdownEvent, WebserverBase server, ILogProvider log)
+ {
+ const string MANAGED_HEAP_STATS = @"
+ Managed Heap Stats
+--------------------------------------
+ Collections:
+ Gen0: {g0} Gen1: {g1} Gen2: {g2}
+
+ Heap:
+ High Watermark: {hw} KB
+ Last GC Heap Size: {hz} KB
+ Current Load: {ld} KB
+ Fragmented: {fb} KB
+
+ Heap Info:
+ Last GC concurrent? {con}
+ Last GC compacted? {comp}
+ Pause time: {pt} %
+ Pending finalizers: {pf}
+ Pinned objects: {po}
+";
+
+ const string HEAPSTATS = @"
+ Unmanaged Heap Stats
+---------------------------
+ userHeap? {rp}
+ Allocated bytes: {ab}
+ Allocated handles: {h}
+ Max block size: {mb}
+ Min block size: {mmb}
+ Max heap size: {hs}
+";
+ const string HELP = @"
+ VNLib.WebServer console help menu
+
+ p <plugin-name> <command> - Sends a command to a plugin
+ cmd <plugin-name> - Enters a command loop for the specified plugin
+ reload - Reloads all plugins
+ memstats - Prints memory stats
+ collect - Flushes server caches, collects, and compacts memory
+ stop - Stops the server
+ help - Prints this help menu
+";
+
+
+ private readonly HttpServiceStack _serviceStack = server.ServiceStack;
+ private readonly IHttpPluginManager _plugins = server.ServiceStack.PluginManager;
+
+
+ /// <summary>
+ /// Listens for commands and processes them in a continuous loop
+ /// </summary>
+ /// <param name="shutdownEvent">A <see cref="ManualResetEvent"/> that is set when the Stop command is received</param>
+ /// <param name="server">The webserver for the current process</param>
+ public void ListenForCommands(TextReader input, TextWriter output, string name)
+ {
+ log.Information("Listening for commands on {con}", name);
+
+ while (shutdownEvent.WaitOne(0) == false)
+ {
+ string[]? s = input.ReadLine()?.Split(' ');
+ if (s == null)
+ {
+ continue;
+ }
+ switch (s[0].ToLower(null))
+ {
+ case "help":
+ output.WriteLine(HELP);
+ break;
+ //handle plugin
+ case "p":
+ {
+ if (s.Length < 3)
+ {
+ output.WriteLine("Plugin name and command are required");
+ break;
+ }
+
+ string message = string.Join(' ', s[2..]);
+
+ bool sent = _plugins.SendCommandToPlugin(s[1], message, StringComparison.OrdinalIgnoreCase);
+
+ if (!sent)
+ {
+ output.WriteLine("Plugin not found");
+ }
+ }
+ break;
+
+ case "cmd":
+ {
+ if (s.Length < 2)
+ {
+ output.WriteLine("Plugin name is required");
+ break;
+ }
+
+ //Enter plugin command loop
+ EnterPluginLoop(input, output, s[1], _plugins);
+ }
+ break;
+ case "reload":
+ {
+ try
+ {
+ //Reload all plugins
+ _plugins.ForceReloadAllPlugins();
+ }
+ catch (Exception ex)
+ {
+ log.Error(ex);
+ }
+ }
+ break;
+ case "memstats":
+ {
+
+
+ //Collect gc info for managed heap stats
+ int gen0 = GC.CollectionCount(0);
+ int gen1 = GC.CollectionCount(1);
+ int gen2 = GC.CollectionCount(2);
+ GCMemoryInfo mi = GC.GetGCMemoryInfo();
+
+ log.Debug(MANAGED_HEAP_STATS,
+ gen0,
+ gen1,
+ gen2,
+ mi.HighMemoryLoadThresholdBytes / 1024,
+ mi.HeapSizeBytes / 1024,
+ mi.MemoryLoadBytes / 1024,
+ mi.FragmentedBytes / 1024,
+ mi.Concurrent,
+ mi.Compacted,
+ mi.PauseTimePercentage,
+ mi.FinalizationPendingCount,
+ mi.PinnedObjectsCount
+ );
+
+ //Get heap stats
+ HeapStatistics hs = MemoryUtil.GetSharedHeapStats();
+
+ //Print unmanaged heap stats
+ log.Debug(HEAPSTATS,
+ MemoryUtil.IsUserDefinedHeap,
+ hs.AllocatedBytes,
+ hs.AllocatedBlocks,
+ hs.MaxBlockSize,
+ hs.MinBlockSize,
+ hs.MaxHeapSize
+ );
+ }
+ break;
+ case "collect":
+ CollectCache(_serviceStack);
+ GC.Collect(2, GCCollectionMode.Forced, false, true);
+ GC.WaitForFullGCComplete();
+ break;
+ case "stop":
+ shutdownEvent.Set();
+ return;
+ }
+ }
+ }
+
+ /*
+ * Function scopes commands as if the user is writing directly to
+ * the plugin. All commands are passed to the plugin manager for
+ * processing.
+ */
+ private static void EnterPluginLoop(
+ TextReader input,
+ TextWriter output,
+ string pluignName,
+ IHttpPluginManager man
+ )
+ {
+ output.WriteLine("Entering plugin {0}. Type 'exit' to leave", pluignName);
+
+ while (true)
+ {
+ output.Write("{0}>", pluignName);
+
+ string? cmdText = input.ReadLine();
+
+ if (string.IsNullOrWhiteSpace(cmdText))
+ {
+ output.WriteLine("Please enter a command or type 'exit' to leave");
+ continue;
+ }
+
+ if (string.Equals(cmdText, "exit", StringComparison.OrdinalIgnoreCase))
+ {
+ break;
+ }
+
+ //Exec command
+ if (!man.SendCommandToPlugin(pluignName, cmdText, StringComparison.OrdinalIgnoreCase))
+ {
+ output.WriteLine("Plugin does not exist exiting loop");
+ break;
+ }
+ }
+ }
+
+ private static void CollectCache(HttpServiceStack controller)
+ => controller.Servers.ForEach(static server => (server as HttpServer)!.CacheClear());
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Compression/FallbackCompressionManager.cs b/apps/VNLib.WebServer/src/Compression/FallbackCompressionManager.cs
new file mode 100644
index 0000000..d2eb719
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Compression/FallbackCompressionManager.cs
@@ -0,0 +1,144 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: FallbackCompressionManager.cs
+*
+* FallbackCompressionManager.cs is part of VNLib.WebServer which is part
+* of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.IO.Compression;
+
+using VNLib.Net.Http;
+
+namespace VNLib.WebServer.Compression
+{
+
+ /*
+ * The fallback compression manager is used when the user did not configure a
+ * compression manager library. Since .NET only exposes a brotli encoder, that
+ * is not a stream api, (gzip and deflate are stream api's) Im only supporting
+ * brotli for now. This is better than nothing lol
+ */
+
+
+ internal sealed class FallbackCompressionManager : IHttpCompressorManager
+ {
+ /// <inheritdoc/>
+ public object AllocCompressor() => new BrCompressorState();
+
+ /// <inheritdoc/>
+ public CompressionMethod GetSupportedMethods() => CompressionMethod.Brotli;
+
+ /// <inheritdoc/>
+ public int InitCompressor(object compressorState, CompressionMethod compMethod)
+ {
+ BrCompressorState compressor = (BrCompressorState)compressorState;
+ ref BrotliEncoder encoder = ref compressor.GetEncoder();
+
+ //Init new brotli encoder struct
+ encoder = new(9, 24);
+ return 0;
+ }
+
+ /// <inheritdoc/>
+ public void DeinitCompressor(object compressorState)
+ {
+ BrCompressorState compressor = (BrCompressorState)compressorState;
+ ref BrotliEncoder encoder = ref compressor.GetEncoder();
+
+ //Clean up the encoder
+ encoder.Dispose();
+ encoder = default;
+ }
+
+ /// <inheritdoc/>
+ public CompressionResult CompressBlock(object compressorState, ReadOnlyMemory<byte> input, Memory<byte> output)
+ {
+ //Output buffer should never be empty, server guards this
+ Debug.Assert(!output.IsEmpty, "Exepcted a non-zero length output buffer");
+
+ BrCompressorState compressor = (BrCompressorState)compressorState;
+ ref BrotliEncoder encoder = ref compressor.GetEncoder();
+
+ //Compress the supplied block
+ OperationStatus status = encoder.Compress(input.Span, output.Span, out int bytesConsumed, out int bytesWritten, false);
+
+ /*
+ * Should always return done, because the output buffer is always
+ * large enough and that data/state cannot be invalid
+ */
+ Debug.Assert(status == OperationStatus.Done);
+
+ return new()
+ {
+ BytesRead = bytesConsumed,
+ BytesWritten = bytesWritten,
+ };
+ }
+
+ /// <inheritdoc/>
+ public int Flush(object compressorState, Memory<byte> output)
+ {
+ OperationStatus status;
+
+ //Output buffer should never be empty, server guards this
+ Debug.Assert(!output.IsEmpty, "Exepcted a non-zero length output buffer");
+
+ BrCompressorState compressor = (BrCompressorState)compressorState;
+ ref BrotliEncoder encoder = ref compressor.GetEncoder();
+
+ /*
+ * A call to compress with the isFinalBlock flag set to true will
+ * cause a BROTLI_OPERATION_FINISH operation to be performed. This is
+ * actually the proper way to complete a brotli compression stream.
+ *
+ * See vnlib_compress project for more details.
+ */
+ status = encoder.Compress(
+ source: default,
+ destination: output.Span,
+ bytesConsumed: out _,
+ bytesWritten: out int bytesWritten,
+ isFinalBlock: true
+ );
+
+ /*
+ * Function can return Done or DestinationTooSmall if there is still more data
+ * stored in the compressor to be written. If InvaliData is returned, then there
+ * is a problem with the encoder state or the output buffer, this condition should
+ * never happen.
+ */
+ Debug.Assert(status != OperationStatus.InvalidData, $"Failed with status {status}, written {bytesWritten}, buffer size {output.Length}");
+
+ //Return the number of bytes actually accumulated
+ return bytesWritten;
+ }
+
+
+ private sealed class BrCompressorState
+ {
+ private BrotliEncoder _encoder;
+
+ public ref BrotliEncoder GetEncoder() => ref _encoder;
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Compression/HttpCompressor.cs b/apps/VNLib.WebServer/src/Compression/HttpCompressor.cs
new file mode 100644
index 0000000..beda67b
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Compression/HttpCompressor.cs
@@ -0,0 +1,123 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: HttpCompressor.cs
+*
+* HttpCompressor.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Reflection;
+using System.Runtime.Loader;
+
+using VNLib.Utils.IO;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Resources;
+using VNLib.Net.Http;
+
+using VNLib.WebServer.Config;
+using VNLib.WebServer.RuntimeLoading;
+using VNLib.WebServer.Config.Model;
+
+namespace VNLib.WebServer.Compression
+{
+
+ internal static class HttpCompressor
+ {
+ /*
+ * A function delegate that is invoked on the user-defined http compressor library
+ * when loaded
+ */
+ private delegate void OnHttpLibLoad(ILogProvider log, JsonElement? configData);
+
+ /// <summary>
+ /// Attempts to load a user-defined http compressor library from the specified path in the config,
+ /// otherwise falls back to the default http compressor, unless the command line disabled compression.
+ /// </summary>
+ /// <param name="args">Process wide- argument list</param>
+ /// <param name="config">The top-level config element</param>
+ /// <param name="logger">The application logger to write logging events to</param>
+ /// <returns>The <see cref="IHttpCompressorManager"/> that the user configured, or null if disabled</returns>
+ public static IHttpCompressorManager? LoadOrDefaultCompressor(ProcessArguments args, HttpCompressorConfig compConfig, IServerConfig config, ILogProvider logger)
+ {
+ const string EXTERN_LIB_LOAD_METHOD_NAME = "OnLoad";
+
+ if (args.HasArgument("--compression-off"))
+ {
+ logger.Information("Http compression disabled by cli args");
+ return null;
+ }
+
+ if(!compConfig.Enabled)
+ {
+ logger.Information("Http compression disabled by config");
+ return null;
+ }
+
+ if (string.IsNullOrWhiteSpace(compConfig.AssemblyPath))
+ {
+ logger.Information("Falling back to default http compressor");
+ return new FallbackCompressionManager();
+ }
+
+ //Make sure the file exists
+ if (!FileOperations.FileExists(compConfig.AssemblyPath))
+ {
+ logger.Warn("The specified http compressor assembly file does not exist, falling back to default http compressor");
+ return new FallbackCompressionManager();
+ }
+
+ //Try to load the assembly into our process alc, we dont need to worry about unloading
+ ManagedLibrary lib = ManagedLibrary.LoadManagedAssembly(compConfig.AssemblyPath, AssemblyLoadContext.Default);
+
+ logger.Debug("Loading user defined compressor assembly: {asm}", Path.GetFileName(lib.AssemblyPath));
+
+ try
+ {
+ //Load the compressor manager type from the assembly
+ IHttpCompressorManager instance = lib.LoadTypeFromAssembly<IHttpCompressorManager>();
+
+ /*
+ * We can provide some optional library initialization functions if the library
+ * supports it. First we can allow the library to write logs to our log provider
+ * and second we can provide the library with the raw configuration data as a byte array
+ */
+
+ //Invoke the on load method with the logger and config data
+ OnHttpLibLoad? onlibLoadConfig = ManagedLibrary.TryGetMethod<OnHttpLibLoad>(instance, EXTERN_LIB_LOAD_METHOD_NAME);
+ onlibLoadConfig?.Invoke(logger, config.GetDocumentRoot());
+
+ //Invoke parameterless on load method
+ Action? onLibLoad = ManagedLibrary.TryGetMethod<Action>(instance, EXTERN_LIB_LOAD_METHOD_NAME);
+ onLibLoad?.Invoke();
+
+ logger.Information("Custom compressor library loaded");
+
+ return instance;
+ }
+ //Catch TIE and throw the inner exception for cleaner debug
+ catch (TargetInvocationException te) when (te.InnerException != null)
+ {
+ throw te.InnerException;
+ }
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Config/IServerConfig.cs b/apps/VNLib.WebServer/src/Config/IServerConfig.cs
new file mode 100644
index 0000000..b0b06ba
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Config/IServerConfig.cs
@@ -0,0 +1,34 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: IServerConfig.cs
+*
+* IServerConfig.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Text.Json;
+
+
+namespace VNLib.WebServer.Config
+{
+ internal interface IServerConfig
+ {
+ JsonElement GetDocumentRoot();
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Config/JsonConfigOptions.cs b/apps/VNLib.WebServer/src/Config/JsonConfigOptions.cs
new file mode 100644
index 0000000..a48b4e9
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Config/JsonConfigOptions.cs
@@ -0,0 +1,42 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: JsonConfigOptions.cs
+*
+* JsonConfigOptions.cs is part of VNLib.WebServer which is part of
+* the larger VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Text.Json;
+
+namespace VNLib.WebServer.Config
+{
+ internal static class JsonConfigOptions
+ {
+ private static readonly JsonSerializerOptions _ops = new()
+ {
+ AllowTrailingCommas = true,
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ };
+
+ public static T? DeserializeElement<T>(this JsonElement el)
+ {
+ return el.Deserialize<T>(_ops);
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Config/JsonServerConfig.cs b/apps/VNLib.WebServer/src/Config/JsonServerConfig.cs
new file mode 100644
index 0000000..ada9902
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Config/JsonServerConfig.cs
@@ -0,0 +1,156 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: JsonServerConfig.cs
+*
+* JsonServerConfig.cs is part of VNLib.WebServer which is part
+* of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Text.Json;
+
+using YamlDotNet.Core.Events;
+using YamlDotNet.Serialization;
+
+using VNLib.Utils.IO;
+
+namespace VNLib.WebServer.Config
+{
+ internal sealed class JsonServerConfig(JsonDocument doc) : IServerConfig
+ {
+ public JsonElement GetDocumentRoot() => doc.RootElement;
+
+ public static JsonServerConfig? FromFile(string filename)
+ {
+ string nameOnly = Path.GetFileName(filename);
+ Console.WriteLine("Loading configuration file from {0}", nameOnly);
+
+ if (filename.EndsWith(".json"))
+ {
+ return FromJson(filename);
+ }
+ else if (filename.EndsWith(".yaml") || filename.EndsWith(".yml"))
+ {
+ return FromYaml(filename);
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Reads a server configuration from the specified JSON document
+ /// </summary>
+ /// <param name="configPath">The file path of the json cofiguration file</param>
+ /// <returns>A new <see cref="JsonServerConfig"/> wrapping the server config</returns>
+ public static JsonServerConfig? FromJson(string fileName)
+ {
+ if (!FileOperations.FileExists(fileName))
+ {
+ return null;
+ }
+
+ //Open the config file
+ using FileStream fs = File.OpenRead(fileName);
+
+ //Allow comments
+ JsonDocumentOptions jdo = new()
+ {
+ CommentHandling = JsonCommentHandling.Skip,
+ AllowTrailingCommas = true,
+ };
+
+ return new JsonServerConfig(JsonDocument.Parse(fs, jdo));
+ }
+
+ public static JsonServerConfig? FromYaml(string fileName)
+ {
+ if (!FileOperations.FileExists(fileName))
+ {
+ return null;
+ }
+
+ /*
+ * The following code reads the configuration as a yaml
+ * object and then serializes it over to json.
+ */
+
+ using StreamReader reader = OpenFileRead(fileName);
+
+ object? yamlObject = new DeserializerBuilder()
+ .WithNodeTypeResolver(new NumberTypeResolver())
+ .Build()
+ .Deserialize(reader);
+
+ ISerializer serializer = new SerializerBuilder()
+ .JsonCompatible()
+ .Build();
+
+ using VnMemoryStream ms = new();
+ using (StreamWriter sw = new(ms, leaveOpen: true))
+ {
+ serializer.Serialize(sw, yamlObject);
+ }
+
+ ms.Seek(0, SeekOrigin.Begin);
+
+ return new JsonServerConfig(JsonDocument.Parse(ms));
+ }
+
+ private static StreamReader OpenFileRead(string fileName)
+ {
+ return new StreamReader(
+ stream: File.OpenRead(fileName),
+ encoding: System.Text.Encoding.UTF8,
+ detectEncodingFromByteOrderMarks: false,
+ leaveOpen: false
+ );
+ }
+
+ public class NumberTypeResolver : INodeTypeResolver
+ {
+ public bool Resolve(NodeEvent? nodeEvent, ref Type currentType)
+ {
+ if (nodeEvent is Scalar scalar)
+ {
+ if(long.TryParse(scalar.Value, out _))
+ {
+ currentType = typeof(int);
+ return true;
+ }
+
+ if (double.TryParse(scalar.Value, out _))
+ {
+ currentType = typeof(double);
+ return true;
+ }
+
+ if (bool.TryParse(scalar.Value, out _))
+ {
+ currentType = typeof(bool);
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Config/Model/BenchmarkConfig.cs b/apps/VNLib.WebServer/src/Config/Model/BenchmarkConfig.cs
new file mode 100644
index 0000000..c569b95
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Config/Model/BenchmarkConfig.cs
@@ -0,0 +1,42 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: BenchmarkConfig.cs
+*
+* BenchmarkConfig.cs is part of VNLib.WebServer which is part of the
+* larger VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Text.Json.Serialization;
+
+namespace VNLib.WebServer.Config.Model
+{
+ internal class BenchmarkConfig
+ {
+
+ [JsonPropertyName("enabled")]
+ public bool Enabled { get; set; }
+
+ [JsonPropertyName("size")]
+ public int Size { get; set; }
+
+ [JsonPropertyName("random")]
+ public bool Random { get; set; }
+
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Config/Model/CorsSecurityConfig.cs b/apps/VNLib.WebServer/src/Config/Model/CorsSecurityConfig.cs
new file mode 100644
index 0000000..2f95697
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Config/Model/CorsSecurityConfig.cs
@@ -0,0 +1,41 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: CorsSecurityConfig.cs
+*
+* CorsSecurityConfig.cs is part of VNLib.WebServer which is part of the
+* larger VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Text.Json.Serialization;
+
+namespace VNLib.WebServer.Config.Model
+{
+ internal class CorsSecurityConfig
+ {
+ [JsonPropertyName("enabled")]
+ public bool Enabled { get; set; } = false;
+
+ [JsonPropertyName("deny_cors_connections")]
+ public bool DenyCorsCons { get; set; } = false;
+
+ [JsonPropertyName("allowed_authority")]
+ public string[] AllowedCorsAuthority { get; set; } = Array.Empty<string>();
+ }
+} \ No newline at end of file
diff --git a/apps/VNLib.WebServer/src/Config/Model/ErrorFileConfig.cs b/apps/VNLib.WebServer/src/Config/Model/ErrorFileConfig.cs
new file mode 100644
index 0000000..c4355d6
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Config/Model/ErrorFileConfig.cs
@@ -0,0 +1,37 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: ErrorFileConfig.cs
+*
+* ErrorFileConfig.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Text.Json.Serialization;
+
+namespace VNLib.WebServer.Config.Model
+{
+ internal sealed class ErrorFileConfig
+ {
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ [JsonPropertyName("path")]
+ public string? Path { get; set; }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Config/Model/HttpCompressorConfig.cs b/apps/VNLib.WebServer/src/Config/Model/HttpCompressorConfig.cs
new file mode 100644
index 0000000..eb8e68c
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Config/Model/HttpCompressorConfig.cs
@@ -0,0 +1,47 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: HttpCompressorConfig.cs
+*
+* HttpCompressorConfig.cs is part of VNLib.WebServer which is part of
+* the larger VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Text.Json.Serialization;
+
+namespace VNLib.WebServer.Config.Model
+{
+ internal sealed class HttpCompressorConfig
+ {
+ [JsonPropertyName("assembly")]
+ public string? AssemblyPath { get; set; }
+
+ /// <summary>
+ /// If this compressor is enabled. The default is true, to use built-in
+ /// compressors.
+ /// </summary>
+ [JsonPropertyName("enabled")]
+ public bool Enabled { get; set; } = true;
+
+ [JsonPropertyName("max_size")]
+ public long CompressionMax { get; set; } = 104857600; //100MB
+
+ [JsonPropertyName("min_size")]
+ public int CompressionMin { get; set; } = 256;
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Config/Model/HttpGlobalConfig.cs b/apps/VNLib.WebServer/src/Config/Model/HttpGlobalConfig.cs
new file mode 100644
index 0000000..22bfe75
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Config/Model/HttpGlobalConfig.cs
@@ -0,0 +1,137 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: HttpGlobalConfig.cs
+*
+* HttpGlobalConfig.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Text.Json.Serialization;
+
+namespace VNLib.WebServer.Config.Model
+{
+ internal class HttpGlobalConfig
+ {
+
+ [JsonPropertyName("default_version")]
+ public string DefaultHttpVersion { get; set; } = "HTTP/1.1";
+
+ /// <summary>
+ /// The maximum size of a request entity that can be sent to the server.
+ /// </summary>
+ [JsonPropertyName("max_entity_size")]
+ public long MaxEntitySize { get; set; } = long.MaxValue;
+
+ /// <summary>
+ /// The maximum size of a multipart form data upload.
+ /// </summary>
+ [JsonPropertyName("multipart_max_size")]
+ public int MultipartMaxSize { get; set; } = 1048576; //1MB
+
+ /// <summary>
+ /// The time in milliseconds for an HTTP/1.1 connection to remain open
+ /// before the server closes it.
+ /// </summary>
+ [JsonPropertyName("keepalive_ms")]
+ public int KeepAliveMs { get; set; } = 60000; //60 seconds
+
+ /// <summary>
+ /// The time in milliseconds to wait for data on an active connection.
+ /// IE: A connection that has been established and has signaled that
+ /// it is ready to transfer data.
+ /// </summary>
+ [JsonPropertyName("recv_timeout_ms")]
+ public int RecvTimeoutMs { get; set; } = 5000; //5 seconds
+
+ /// <summary>
+ /// The time in milliseconds to wait for data to be sent on a connection.
+ /// </summary>
+ [JsonPropertyName("send_timeout_ms")]
+ public int SendTimeoutMs { get; set; } = 60000; //60 seconds
+
+ /// <summary>
+ /// The maximum number of headers that can be sent in a request.
+ /// </summary>
+ [JsonPropertyName("max_request_header_count")]
+ public int MaxRequestHeaderCount { get; set; } = 32;
+
+ /// <summary>
+ /// The maximum number of open connections that can be made to the server, before
+ /// the server starts rejecting new connections.
+ /// </summary>
+ [JsonPropertyName("max_connections")]
+ public int MaxConnections { get; set; } = int.MaxValue;
+
+ /// <summary>
+ /// The maximum number of uploads that can be made in a single request. If
+ /// this value is exceeded, the request will be rejected.
+ /// </summary>
+ [JsonPropertyName("max_uploads_per_request")]
+ public ushort MaxUploadsPerRequest { get; set; } = 10;
+
+ /// <summary>
+ /// The size of the buffer used to store request headers.
+ /// </summary>
+ [JsonPropertyName("header_buf_size")]
+ public int HeaderBufSize { get; set; }
+
+ /// <summary>
+ /// The size of the buffer used to store response headers.
+ /// </summary>
+ [JsonPropertyName("response_header_buf_size")]
+ public int ResponseHeaderBufSize { get; set; }
+
+ /// <summary>
+ /// The size of the buffer used to store form data.
+ /// </summary>
+ [JsonPropertyName("multipart_max_buf_size")]
+ public int MultipartMaxBufSize { get; set; }
+
+ /// <summary>
+ /// The configuration for the HTTP compression settings.
+ /// </summary>
+ [JsonPropertyName("compression")]
+ public HttpCompressorConfig? Compression { get; set; } = new();
+
+ public void ValidateConfig()
+ {
+ Validate.EnsureNotNull(DefaultHttpVersion, "Default HTTP version is required");
+
+ Validate.EnsureRange(MaxEntitySize, 0, long.MaxValue);
+ Validate.EnsureRange(MultipartMaxSize, -1, int.MaxValue);
+ Validate.EnsureRange(KeepAliveMs, -1, int.MaxValue);
+
+ //Timeouts may be disabled by setting 0 or -1. Both are allowed for readability
+ Validate.EnsureRange(RecvTimeoutMs, -2, int.MaxValue);
+ Validate.EnsureRange(SendTimeoutMs, -2, int.MaxValue);
+
+ Validate.EnsureRange(MaxRequestHeaderCount, 0, 1024);
+ Validate.EnsureRange(MaxConnections, 0, int.MaxValue);
+ Validate.EnsureRange(MaxUploadsPerRequest, 0, 1024);
+ Validate.EnsureRange(HeaderBufSize, 0, int.MaxValue);
+ Validate.EnsureRange(ResponseHeaderBufSize, 0, int.MaxValue);
+ Validate.EnsureRange(MultipartMaxBufSize, 0, int.MaxValue);
+
+ //Validate compression config
+ Validate.EnsureNotNull(Compression, "Compression configuration should not be set to null. Comment to enable defaults");
+ Validate.EnsureRange(Compression.CompressionMax, -1, long.MaxValue);
+ Validate.EnsureRange(Compression.CompressionMin, -1, int.MaxValue);
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Config/Model/ServerPluginConfig.cs b/apps/VNLib.WebServer/src/Config/Model/ServerPluginConfig.cs
new file mode 100644
index 0000000..42b91b1
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Config/Model/ServerPluginConfig.cs
@@ -0,0 +1,46 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: ServerPluginConfig.cs
+*
+* ServerPluginConfig.cs is part of VNLib.WebServer which is part of
+* the larger VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Text.Json.Serialization;
+
+namespace VNLib.WebServer.Config.Model
+{
+ internal class ServerPluginConfig
+ {
+ [JsonPropertyName("enabled")]
+ public bool Enabled { get; set; } = true; //default to true if config is defined, then it can be assumed we want to load plugins unless explicitly disabled
+
+ [JsonPropertyName("path")]
+ public string? Path { get; set; }
+
+ [JsonPropertyName("config_dir")]
+ public string? ConfigDir { get; set; }
+
+ [JsonPropertyName("hot_reload")]
+ public bool HotReload { get; set; }
+
+ [JsonPropertyName("reload_delay_sec")]
+ public int ReloadDelaySec { get; set; } = 2;
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Config/Model/TcpConfigJson.cs b/apps/VNLib.WebServer/src/Config/Model/TcpConfigJson.cs
new file mode 100644
index 0000000..5bd2b94
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Config/Model/TcpConfigJson.cs
@@ -0,0 +1,73 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: TcpServerLoader.cs
+*
+* TcpServerLoader.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Text.Json.Serialization;
+
+namespace VNLib.WebServer.Config.Model
+{
+ internal sealed class TcpConfigJson
+ {
+ [JsonPropertyName("keepalive_sec")]
+ public int TcpKeepAliveTime { get; set; } = 4;
+
+ [JsonPropertyName("keepalive_interval_sec")]
+ public int KeepaliveInterval { get; set; } = 4;
+
+ [JsonPropertyName("max_recv_buffer")]
+ public int MaxRecvBufferData { get; set; } = 10 * 64 * 1024;
+
+ [JsonPropertyName("backlog")]
+ public int BackLog { get; set; } = 1000;
+
+ [JsonPropertyName("max_connections")]
+ public long MaxConnections { get; set; } = long.MaxValue;
+
+ [JsonPropertyName("no_delay")]
+ public bool NoDelay { get; set; } = false;
+
+ /*
+ * Buffer sizes are a pain, this is a good default size for medium bandwith connections (100mbps)
+ * using the BDP calculations
+ *
+ * BDP = Bandwidth * RTT
+ */
+
+ [JsonPropertyName("tx_buffer")]
+ public int TcpSendBufferSize { get; set; } = 625 * 1024;
+
+ [JsonPropertyName("rx_buffer")]
+ public int TcpRecvBufferSize { get; set; } = 625 * 1024;
+
+
+ public void ValidateConfig()
+ {
+ Validate.EnsureRange(TcpKeepAliveTime, 0, 60);
+ Validate.EnsureRange(KeepaliveInterval, 0, 60);
+ Validate.EnsureRange(BackLog, 0, 10000);
+ Validate.EnsureRange(MaxConnections, 0, long.MaxValue);
+ Validate.EnsureRange(MaxRecvBufferData, 0, 10 * 1024 * 1024); //10MB
+ Validate.EnsureRange(TcpSendBufferSize, 0, 10 * 1024 * 1024); //10MB
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Config/Model/TransportInterface.cs b/apps/VNLib.WebServer/src/Config/Model/TransportInterface.cs
new file mode 100644
index 0000000..e0e2551
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Config/Model/TransportInterface.cs
@@ -0,0 +1,137 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: TransportInterface.cs
+*
+* TransportInterface.cs is part of VNLib.WebServer which is part of
+* the larger VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Net;
+using System.Security.Cryptography.X509Certificates;
+using System.Text.Json.Serialization;
+
+using VNLib.Utils.Memory;
+using VNLib.Utils.Resources;
+
+namespace VNLib.WebServer.Config.Model
+{
+ /// <summary>
+ /// Represents a transport interface configuration element for a virtual host
+ /// </summary>
+ internal class TransportInterface
+ {
+ [JsonPropertyName("port")]
+ public int Port { get; set; }
+
+ [JsonPropertyName("address")]
+ public string? Address { get; set; }
+
+ [JsonPropertyName("certificate")]
+ public string? Cert { get; set; }
+
+ [JsonPropertyName("private_key")]
+ public string? PrivKey { get; set; }
+
+ [JsonPropertyName("ssl")]
+ public bool Ssl { get; set; }
+
+ [JsonPropertyName("client_cert_required")]
+ public bool ClientCertRequired { get; set; }
+
+ [JsonPropertyName("password")]
+ public string? PrivKeyPassword { get; set; }
+
+ [JsonPropertyName("use_os_ciphers")]
+ public bool UseOsCiphers { get; set; }
+
+ public IPEndPoint GetEndpoint()
+ {
+ IPAddress addr = string.IsNullOrEmpty(Address) ? IPAddress.Any : IPAddress.Parse(Address);
+ return new IPEndPoint(addr, Port);
+ }
+
+ public X509Certificate? LoadCertificate()
+ {
+ if (!Ssl)
+ {
+ return null;
+ }
+
+ Validate.EnsureNotNull(Cert, "TLS Certificate is required when ssl is enabled");
+ Validate.FileExists(Cert);
+
+ X509Certificate? cert = null;
+
+ /*
+ * Default to use a PEM encoded certificate and private key file. Unless the file
+ * is a pfx file, then we will use the private key from the pfx file.
+ */
+
+ if (Path.GetExtension(Cert).EndsWith("pfx", StringComparison.OrdinalIgnoreCase))
+ {
+ //Create from pfx file including private key
+ cert = X509Certificate.CreateFromCertFile(Cert);
+ }
+ else
+ {
+ Validate.EnsureNotNull(PrivKey, "TLS Private Key is required ssl is enabled");
+ Validate.FileExists(PrivKey);
+
+ /*
+ * Attempt to capture the private key password. This will wrap the
+ * string in a private string instance, and setting the value to true
+ * will ensure the password memory is wiped when this function returns
+ */
+ using PrivateString? password = PrivateString.ToPrivateString(PrivKeyPassword, true);
+
+ //Load the cert and decrypt with password if set
+ using X509Certificate2 cert2 = password == null ? X509Certificate2.CreateFromPemFile(Cert, PrivKey)
+ : X509Certificate2.CreateFromEncryptedPemFile(Cert, password.ToReadOnlySpan(), PrivKey);
+
+ /*
+ * Workaround for a silly Windows SecureChannel module bug for parsing
+ * X509Certificate2 from pem cert and private key files.
+ *
+ * Must export into pkcs12 format then create a new X509Certificate2 from the
+ * exported bytes.
+ */
+
+ //Copy the cert in pkcs12 format
+ byte[] pkcs = cert2.Export(X509ContentType.Pkcs12);
+ cert = new X509Certificate2(pkcs);
+ MemoryUtil.InitializeBlock(pkcs);
+ }
+
+ return cert;
+ }
+
+ /// <summary>
+ /// Builds a deterministic hash-code base on the configuration state.
+ /// </summary>
+ /// <returns>The hash-code that represents the current instance</returns>
+ public override int GetHashCode() => HashCode.Combine(Address, Port);
+
+ public override bool Equals(object? obj) => obj is TransportInterface iface && GetHashCode() == iface.GetHashCode();
+
+ public override string ToString() => $"[{Address}:{Port}]";
+
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Config/Model/VirtualHostServerConfig.cs b/apps/VNLib.WebServer/src/Config/Model/VirtualHostServerConfig.cs
new file mode 100644
index 0000000..2f18cf7
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Config/Model/VirtualHostServerConfig.cs
@@ -0,0 +1,96 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: VirtualHostServerConfig.cs
+*
+* VirtualHostServerConfig.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace VNLib.WebServer.Config.Model
+{
+ internal sealed class VirtualHostServerConfig
+ {
+ [JsonPropertyName("trace")]
+ public bool RequestTrace { get; set; } = false;
+
+ [JsonPropertyName("force_port_check")]
+ public bool ForcePortCheck { get; set; } = false;
+
+ [JsonPropertyName("benchmark")]
+ public BenchmarkConfig? Benchmark { get; set; }
+
+ [JsonPropertyName("interfaces")]
+ public TransportInterface[] Interfaces { get; set; } = Array.Empty<TransportInterface>();
+
+ [JsonPropertyName("hostnames")]
+ public string[]? Hostnames { get; set; } = Array.Empty<string>();
+
+ [JsonPropertyName("hostname")]
+ public string? Hostname
+ {
+ get => Hostnames?.FirstOrDefault();
+ set
+ {
+ if (value != null)
+ {
+ Hostnames = [value];
+ }
+ }
+ }
+
+ [JsonPropertyName("path")]
+ public string? DirPath { get; set; } = string.Empty;
+
+ [JsonPropertyName("downstream_servers")]
+ public string[] DownstreamServers { get; set; } = Array.Empty<string>();
+
+ [JsonPropertyName("whitelist")]
+ public string[]? Whitelist { get; set; }
+
+ [JsonPropertyName("blacklist")]
+ public string[]? Blacklist { get; set; }
+
+ [JsonPropertyName("deny_extensions")]
+ public string[]? DenyExtensions { get; set; }
+
+ [JsonPropertyName("default_files")]
+ public string[]? DefaultFiles { get; set; }
+
+ [JsonPropertyName("headers")]
+ public Dictionary<string, string> Headers { get; set; } = [];
+
+ [JsonPropertyName("cors")]
+ public CorsSecurityConfig Cors { get; set; } = new();
+
+ [JsonPropertyName("error_files")]
+ public ErrorFileConfig[] ErrorFiles { get; set; } = Array.Empty<ErrorFileConfig>();
+
+ [JsonPropertyName("cache_default_sec")]
+ public int CacheDefaultTimeSeconds { get; set; } = 0;
+
+ [JsonPropertyName("path_filter")]
+ public string? PathFilter { get; set; }
+
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Config/ServerConfigurationException.cs b/apps/VNLib.WebServer/src/Config/ServerConfigurationException.cs
new file mode 100644
index 0000000..39c18e5
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Config/ServerConfigurationException.cs
@@ -0,0 +1,40 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: ServerConfigurationException.cs
+*
+* ServerConfigurationException.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.WebServer.Config
+{
+ public class ServerConfigurationException : Exception
+ {
+ public ServerConfigurationException()
+ { }
+
+ public ServerConfigurationException(string? message) : base(message)
+ { }
+
+ public ServerConfigurationException(string? message, Exception? innerException) : base(message, innerException)
+ { }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Config/Validate.cs b/apps/VNLib.WebServer/src/Config/Validate.cs
new file mode 100644
index 0000000..773d787
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Config/Validate.cs
@@ -0,0 +1,113 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: Validate.cs
+*
+* Validate.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Net;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+using VNLib.Utils.IO;
+
+namespace VNLib.WebServer.Config
+{
+ internal static class Validate
+ {
+ [DoesNotReturn]
+ public static void EnsureNotNull<T>(T? obj, string message) where T : class
+ {
+ if (obj is null)
+ {
+ throw new ServerConfigurationException(message);
+ }
+
+ if (obj is string s && string.IsNullOrWhiteSpace(s))
+ {
+ throw new ServerConfigurationException(message);
+ }
+ }
+
+ public static void Assert([DoesNotReturnIf(false)] bool condition, string message)
+ {
+ if (!condition)
+ {
+ throw new ServerConfigurationException(message);
+ }
+ }
+
+ public static void EnsureValidIp(string? address, string message)
+ {
+ if (!IPAddress.TryParse(address, out _))
+ {
+ throw new ServerConfigurationException(message);
+ }
+ }
+
+ public static void EnsureNotEqual<T>(T a, T b, string message)
+ {
+ if (a is null || b is null)
+ {
+ throw new ServerConfigurationException(message);
+ }
+
+ if (a.Equals(b))
+ {
+ throw new ServerConfigurationException(message);
+ }
+ }
+
+ public static void EnsureRangeEx(ulong value, ulong min, ulong max, string message)
+ {
+ if (value < min || value > max)
+ {
+ throw new ServerConfigurationException(message);
+ }
+ }
+
+ public static void EnsureRangeEx(long value, long min, long max, string message)
+ {
+ if (value < min || value > max)
+ {
+ throw new ServerConfigurationException(message);
+ }
+ }
+
+ public static void EnsureRange(ulong value, ulong min, ulong max, [CallerArgumentExpression(nameof(value))] string? paramName = null)
+ {
+ EnsureRangeEx(value, min, max, $"Value for {paramName} must be between {min} and {max}. Value: {value}");
+ }
+
+ public static void EnsureRange(long value, long min, long max, [CallerArgumentExpression(nameof(value))] string? paramName = null)
+ {
+ EnsureRangeEx(value, min, max, $"Value for {paramName} must be between {min} and {max}. Value: {value}");
+ }
+
+ public static void FileExists(string path)
+ {
+ if (!FileOperations.FileExists(path))
+ {
+ throw new ServerConfigurationException($"Required file: {path} not found");
+ }
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Entry.cs b/apps/VNLib.WebServer/src/Entry.cs
new file mode 100644
index 0000000..731e4f4
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Entry.cs
@@ -0,0 +1,336 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: Entry.cs
+*
+* Entry.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Net.Sockets;
+using System.Runtime.ExceptionServices;
+
+using VNLib.Utils.IO;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Logging;
+using VNLib.Hashing;
+using VNLib.Hashing.Native.MonoCypher;
+
+using VNLib.WebServer.Config;
+using VNLib.WebServer.Bootstrap;
+using VNLib.WebServer.RuntimeLoading;
+
+namespace VNLib.WebServer
+{
+
+ static class Entry
+ {
+ const string STARTUP_MESSAGE =
+@"VNLib.Webserver - runtime host Copyright (C) Vaughn Nugent
+This program comes with ABSOLUTELY NO WARRANTY.
+Licensing for this software and other libraries can be found at https://www.vaughnnugent.com/resources/software
+Starting...
+";
+
+ private static readonly DirectoryInfo EXE_DIR = new(Environment.CurrentDirectory);
+
+ private const string DEFAULT_CONFIG_PATH = "config.json";
+ internal const string SESSION_TIMEOUT_PROP_NAME = "max_execution_time_ms";
+ internal const string TCP_CONF_PROP_NAME = "tcp";
+ internal const string LOAD_DEFAULT_HOSTNAME_VALUE = "[system]";
+ internal const string PLUGINS_CONFIG_PROP_NAME = "plugins";
+
+
+ static int Main(string[] args)
+ {
+ ProcessArguments procArgs = new(args);
+
+ //Print the help menu
+ if (args.Length == 0 || procArgs.HasArgument("-h") || procArgs.HasArgument("--help"))
+ {
+ PrintHelpMenu();
+ return 0;
+ }
+
+ Console.WriteLine(STARTUP_MESSAGE);
+
+ //Init log config builder
+ ServerLogBuilder logBuilder = new();
+ logBuilder.BuildForConsole(procArgs);
+
+ //try to load the json configuration file
+ IServerConfig? config = LoadConfig(procArgs);
+ if (config is null)
+ {
+ logBuilder.AppLogConfig.CreateLogger().Error("No configuration file was found");
+ return -1;
+ }
+
+ //Build logs from config
+ logBuilder.BuildFromConfig(config.GetDocumentRoot());
+
+ //Create the logger
+ using ServerLogger logger = logBuilder.GetLogger();
+
+ //Dump config to console
+ if (procArgs.HasArgument("--dump-config"))
+ {
+ DumpConfig(config.GetDocumentRoot(), logger);
+ }
+
+ //Setup the app-domain listener
+ InitAppDomainListener(procArgs, logger.AppLog);
+
+#if !DEBUG
+ if (procArgs.LogHttp)
+ {
+ logger.AppLog.Warn("HTTP Logging is only enabled in builds compiled with DEBUG symbols");
+ }
+#endif
+
+ if (procArgs.ZeroAllocations && !MemoryUtil.Shared.CreationFlags.HasFlag(HeapCreation.GlobalZero))
+ {
+ logger.AppLog.Debug("Zero allocation flag was set, but the shared heap was not created with the GlobalZero flag, consider enabling zero allocations globally");
+ }
+
+ using WebserverBase server = GetWebserver(logger, config, procArgs);
+
+ try
+ {
+ logger.AppLog.Information("Building service stack, populating service domain...");
+
+ server.Configure();
+ }
+ catch (ServerConfigurationException sce) when (sce.InnerException is not null)
+ {
+ logger.AppLog.Fatal("Failed to configure server. Reason: {sce}", sce.InnerException.Message);
+ return -1;
+ }
+ catch (ServerConfigurationException sce)
+ {
+ logger.AppLog.Fatal("Failed to configure server. Reason: {sce}", sce.Message);
+ return -1;
+ }
+ catch (Exception ex) when (ex.InnerException is ServerConfigurationException sce)
+ {
+ logger.AppLog.Fatal("Failed to configure server. Reason: {sce}", sce.Message);
+ return -1;
+ }
+ catch (Exception ex)
+ {
+ logger.AppLog.Fatal(ex, "Failed to configure server");
+ return -1;
+ }
+
+ logger.AppLog.Verbose("Server configuration stage complete");
+
+ using ManualResetEvent ShutdownEvent = new(false);
+
+ try
+ {
+ logger.AppLog.Information("Starting services...");
+
+ server.Start();
+
+ logger.AppLog.Information("Service stack started, servers are listening.");
+
+ //Register console cancel to cause cleanup
+ Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) =>
+ {
+ e.Cancel = true;
+ ShutdownEvent.Set();
+ };
+
+ /*
+ * Optional background thread to listen for commands on stdin which
+ * can also request a server shutdown.
+ *
+ * The loop runs in a background thread and will not block the main thread
+ * The loop can request a server shutdown by setting the shutdown event
+ */
+
+ if (!procArgs.HasArgument("--input-off"))
+ {
+ CommandListener cmdLoop = new(ShutdownEvent, server, logger.AppLog);
+
+ Thread consoleListener = new(() => cmdLoop.ListenForCommands(Console.In, Console.Out, name: "stdin"))
+ {
+ IsBackground = true
+ };
+
+ consoleListener.Start();
+ }
+
+ logger.AppLog.Information("Main thread waiting for exit signal, press ctrl + c to exit");
+
+ //Wait for user signal to exit
+ ShutdownEvent.WaitOne();
+
+ logger.AppLog.Information("Stopping service stack");
+
+ server.Stop();
+
+ //Wait for all plugins to unload and cleanup (temporary)
+ Thread.Sleep(500);
+
+ return 0;
+ }
+ catch (SocketException se) when (se.SocketErrorCode == SocketError.AddressAlreadyInUse)
+ {
+ logger.AppLog.Fatal("Failed to start servers, address already in use");
+ return (int)se.SocketErrorCode;
+ }
+ catch (SocketException se)
+ {
+ logger.AppLog.Fatal(se, "Failed to start servers due to a socket exception");
+ return (int)se.SocketErrorCode;
+ }
+ catch (Exception ex)
+ {
+ logger.AppLog.Fatal(ex, "Failed to start web servers");
+ }
+
+ return -1;
+ }
+
+ static void PrintHelpMenu()
+ {
+ const string TEMPLATE =
+@$"
+ VNLib.Webserver Copyright (C) 2024 Vaughn Nugent
+
+ A high-performance, cross-platform, single process, reference webserver built on the .NET 8.0 Core runtime.
+
+ Option flags:
+ --config <path> - Specifies the path to the configuration file (relative or absolute)
+ --input-off - Disables the STDIN listener, no runtime commands will be processed
+ --inline-scheduler - Enables inline scheduling for TCP transport IO processing (not available when using TLS)
+ --no-plugins - Disables loading of dynamic plugins
+ --log-http - Enables logging of HTTP request and response headers to the system logger (debug builds only)
+ --log-transport - Enables logging of transport events to the system logger (debug builds only)
+ --dump-config - Dumps the JSON configuration to the console during loading
+ --compression-off - Disables dynamic response compression
+ --zero-alloc - Forces all http/tcp memory pool allocations to be zeroed before use (reduced performance)
+ --sequential-load - Loads all plugins sequentially (default is concurrently)
+ --no-reuse-socket - Disables socket reuse for TCP connections (Windows only)
+ --reuse-address - Enables address reuse for TCP connections
+ -h, --help - Prints this help menu
+ -t, --threads <num> - Specifies the number of socket accept threads. Defaults to processor count
+ -s, --silent - Disables all console logging
+ -v, --verbose - Enables verbose logging
+ -d, --debug - Enables debug logging for the process and all plugins
+ -vv - Enables very verbose logging (attaches listeners for app-domain events and logs them to the output)
+
+ Your configuration file must be a JSON or YAML encoded file and be readable to the process. You may consider keeping it in a safe
+ location outside the application and only readable to this process.
+
+ You should disable hot-reload for production environments, for security and performance reasons.
+
+ You may consider using the --input-off flag to disable STDIN listening for production environments for security reasons.
+
+ Optional environment variables:
+ {MemoryUtil.SHARED_HEAP_FILE_PATH} - Specifies the path to the native heap allocator library
+ {MemoryUtil.SHARED_HEAP_ENABLE_DIAGNOISTICS_ENV} - Enables heap diagnostics for the shared heap 1 = enabled, 0 = disabled
+ {MemoryUtil.SHARED_HEAP_GLOBAL_ZERO} - Enables zeroing of all allocations from the shared heap 1 = enabled, 0 = disabled
+ {MemoryUtil.SHARED_HEAP_RAW_FLAGS} - Raw flags to pass to the shared heap allocator's HeapCreate function, hexadeciaml encoded
+ {VnArgon2.ARGON2_LIB_ENVIRONMENT_VAR_NAME} - Specifies the path to the Argon2 native library
+ {MonoCypherLibrary.MONOCYPHER_LIB_ENVIRONMENT_VAR_NAME} - Specifies the path to the Monocypher native library
+
+ Usage:
+ VNLib.Webserver --config <path> ... (other options) #Starts the server from the configuration (basic usage)
+
+";
+ Console.WriteLine(TEMPLATE);
+ }
+
+ #region config
+
+ /// <summary>
+ /// Initializes the configuration DOM from the specified cmd args
+ /// or the default configuration path
+ /// </summary>
+ /// <param name="args">The command-line-arguments</param>
+ /// <returns>A new <see cref="JsonDocument"/> that contains the application configuration</returns>
+ private static IServerConfig? LoadConfig(ProcessArguments args)
+ {
+ //Get the config path or default config
+ string configPath = args.GetArgument("--config") ?? Path.Combine(EXE_DIR.FullName, DEFAULT_CONFIG_PATH);
+
+ return JsonServerConfig.FromFile(configPath);
+ }
+
+ private static WebserverBase GetWebserver(ServerLogger logger, IServerConfig config, ProcessArguments procArgs)
+ {
+ logger.AppLog.Information("Configuring production webserver");
+ return new ReleaseWebserver(logger, config, procArgs);
+ }
+
+ private static void DumpConfig(JsonElement doc, ServerLogger logger)
+ {
+ //Dump the config to the console
+ using VnMemoryStream ms = new();
+ using (Utf8JsonWriter writer = new(ms, new() { Indented = true }))
+ {
+ doc.WriteTo(writer);
+ }
+
+ string json = Encoding.UTF8.GetString(ms.AsSpan());
+ logger.AppLog.Information("Dumping configuration to console...\n{c}", json);
+ }
+
+ #endregion
+
+ private static void InitAppDomainListener(ProcessArguments args, ILogProvider log)
+ {
+ AppDomain currentDomain = AppDomain.CurrentDomain;
+ currentDomain.UnhandledException += delegate (object sender, UnhandledExceptionEventArgs e)
+ {
+ log.Fatal("UNHANDLED APPDOMAIN EXCEPTION \n {e}", e);
+ };
+ //If double verbose is specified, log app-domain messages
+ if (args.DoubleVerbose)
+ {
+ log.Verbose("Double verbose mode enabled, registering app-domain listeners");
+
+ currentDomain.FirstChanceException += delegate (object? sender, FirstChanceExceptionEventArgs e)
+ {
+ log.Verbose(e.Exception, "Exception occured in app-domain ");
+ };
+ currentDomain.AssemblyLoad += delegate (object? sender, AssemblyLoadEventArgs args)
+ {
+ log.Verbose(
+ "Assembly loaded {asm} to appdomain {domain} from\n{location}",
+ args.LoadedAssembly.FullName,
+ currentDomain.FriendlyName,
+ args.LoadedAssembly.Location
+ );
+ };
+ currentDomain.DomainUnload += delegate (object? sender, EventArgs e)
+ {
+ log.Verbose("Domain {domain} unloaded", currentDomain.FriendlyName);
+ };
+ }
+ }
+
+ }
+}
diff --git a/apps/VNLib.WebServer/src/MemoryPoolManager.cs b/apps/VNLib.WebServer/src/MemoryPoolManager.cs
new file mode 100644
index 0000000..577bdb2
--- /dev/null
+++ b/apps/VNLib.WebServer/src/MemoryPoolManager.cs
@@ -0,0 +1,137 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: MemoryPoolManager.cs
+*
+* MemoryPoolManager.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+using VNLib.Utils.Memory;
+using VNLib.Net.Http;
+
+namespace VNLib.WebServer
+{
+ /// <summary>
+ /// recovers a memory pool for the TCP server to alloc buffers from
+ /// </summary>
+ internal static class MemoryPoolManager
+ {
+ /// <summary>
+ /// Gets an unmanaged memory pool provider for the TCP server to alloc buffers from
+ /// </summary>
+ /// <returns>The memory pool</returns>
+ public static MemoryPool<byte> GetTcpPool(bool zeroOnAlloc) => new HttpMemoryPool(zeroOnAlloc);
+
+ /// <summary>
+ /// Gets a memory pool provider for the HTTP server to alloc buffers from
+ /// </summary>
+ /// <returns>The http server memory pool</returns>
+ public static IHttpMemoryPool GetHttpPool(bool zeroOnAlloc) => new HttpMemoryPool(zeroOnAlloc);
+
+ /*
+ * Fun little umnanaged memory pool that allows for allocating blocks
+ * with fast pointer access and zero cost pinning
+ *
+ * All blocks are allocated to the nearest page size
+ */
+
+ internal sealed class HttpMemoryPool(bool zeroOnAlloc) : MemoryPool<byte>, IHttpMemoryPool
+ {
+ //Avoid the shared getter on every alloc call
+ private readonly IUnmangedHeap _heap = MemoryUtil.Shared;
+
+ ///<inheritdoc/>
+ public override int MaxBufferSize { get; } = int.MaxValue;
+
+ ///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public IMemoryOwner<byte> AllocateBufferForContext(int bufferSize) => Rent(bufferSize);
+
+ ///<inheritdoc/>
+ public IResizeableMemoryHandle<T> AllocFormDataBuffer<T>(int initialSize) where T : unmanaged
+ {
+ return MemoryUtil.SafeAllocNearestPage<T>(_heap, initialSize, zeroOnAlloc);
+ }
+
+ ///<inheritdoc/>
+ public override IMemoryOwner<byte> Rent(int minBufferSize = -1)
+ {
+ nint initSize = MemoryUtil.NearestPage(minBufferSize);
+ return new UnsafeMemoryManager(_heap, (nuint)initSize, zeroOnAlloc);
+ }
+
+ ///<inheritdoc/>
+ protected override void Dispose(bool disposing)
+ { }
+
+ sealed class UnsafeMemoryManager(IUnmangedHeap heap, nuint bufferSize, bool zero) : MemoryManager<byte>
+ {
+
+ private nint _pointer = heap.Alloc(bufferSize, sizeof(byte), zero);
+ private int _size = (int)bufferSize;
+
+ ///<inheritdoc/>
+ public override Span<byte> GetSpan()
+ {
+ Debug.Assert(_pointer != nint.Zero, "Pointer to memory block is null, was not allocated properly or was released");
+
+ return MemoryUtil.GetSpan<byte>(_pointer, _size);
+ }
+
+ ///<inheritdoc/>
+ public override MemoryHandle Pin(int elementIndex = 0)
+ {
+ //Guard
+ ArgumentOutOfRangeException.ThrowIfNegative(elementIndex);
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(elementIndex, _size);
+
+ Debug.Assert(_pointer != nint.Zero, "Pointer to memory block is null, was not allocated properly or was released");
+
+ //Get pointer offset from index
+ nint offset = nint.Add(_pointer, elementIndex);
+
+ //Return handle at offser
+ return MemoryUtil.GetMemoryHandleFromPointer(offset, pinnable: this);
+ }
+
+ //No-op
+ public override void Unpin()
+ { }
+
+ protected override void Dispose(bool disposing)
+ {
+ Debug.Assert(_pointer != nint.Zero, "Pointer to memory block is null, was not allocated properly");
+
+ bool freed = heap.Free(ref _pointer);
+
+ //Free the memory, should also zero the pointer
+ Debug.Assert(freed, "Failed to free an allocated block");
+
+ //Set size to 0
+ _size = 0;
+ }
+ }
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Middlewares/BenchmarkMiddleware.cs b/apps/VNLib.WebServer/src/Middlewares/BenchmarkMiddleware.cs
new file mode 100644
index 0000000..2e2020c
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Middlewares/BenchmarkMiddleware.cs
@@ -0,0 +1,102 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: BenchmarkMiddleware.cs
+*
+* BenchmarkMiddleware.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+
+using System;
+using System.Net;
+using System.Buffers;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using System.Security.Cryptography;
+
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+using VNLib.Net.Http;
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.Middleware;
+
+using VNLib.WebServer.Config.Model;
+
+namespace VNLib.WebServer.Middlewares
+{
+ /*
+ * This is a cheatsy little syntethic benchmark middleware that will
+ * return a fixed size memory response for every request to simulate
+ * a file response for synthetic benchmarking.
+ *
+ * The buffer may optionally be filled with random data to put a
+ * load on the compressor instead of a zero filled buffer
+ */
+
+ internal sealed class BenchmarkMiddleware(BenchmarkConfig config) : IHttpMiddleware
+ {
+ private readonly MemoryManager<byte> data = AllocBuffer(config.Size, config.Random);
+
+ public ValueTask<FileProcessArgs> ProcessAsync(HttpEntity entity)
+ {
+ entity.CloseResponse(
+ HttpStatusCode.OK,
+ ContentType.Binary,
+ new BenchmarkResponseData(data.Memory)
+ );
+
+ return ValueTask.FromResult(FileProcessArgs.VirtualSkip);
+ }
+
+ private static MemoryManager<byte> AllocBuffer(int size, bool random)
+ {
+ /*
+ * Even though this is testing, the buffer is zeroed to avoid leaking
+ * any data that may be in heap memory after allocation.
+ */
+ MemoryManager<byte> man = MemoryUtil.Shared.DirectAlloc<byte>(size, true);
+
+ if (random)
+ {
+ RandomNumberGenerator.Fill(man.GetSpan());
+ }
+
+ return man;
+ }
+
+ private sealed class BenchmarkResponseData(ReadOnlyMemory<byte> data) : IMemoryResponseReader
+ {
+ int read;
+ readonly int len = data.Length;
+
+ public int Remaining => len - read;
+
+ public void Advance(int written)
+ {
+ read += written;
+ Debug.Assert(Remaining >= 0);
+ }
+
+ public void Close()
+ { }
+
+ public ReadOnlyMemory<byte> GetMemory() => data.Slice(read, Remaining);
+ }
+ }
+} \ No newline at end of file
diff --git a/apps/VNLib.WebServer/src/Middlewares/CORSMiddleware.cs b/apps/VNLib.WebServer/src/Middlewares/CORSMiddleware.cs
new file mode 100644
index 0000000..08b7cee
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Middlewares/CORSMiddleware.cs
@@ -0,0 +1,182 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: CORSMiddleware.cs
+*
+* CORSMiddleware.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using System.Collections.Frozen;
+
+using VNLib.Net.Http;
+using VNLib.Utils.Logging;
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.Sessions;
+using VNLib.Plugins.Essentials.Extensions;
+using VNLib.Plugins.Essentials.Middleware;
+
+using VNLib.WebServer.Config.Model;
+
+namespace VNLib.WebServer.Middlewares
+{
+
+ /// <summary>
+ /// Adds HTTP CORS protection to http servers
+ /// </summary>
+ /// <param name="Log"></param>
+ /// <param name="VirtualHostOptions"></param>
+ [MiddlewareImpl(MiddlewareImplOptions.SecurityCritical)]
+ internal sealed class CORSMiddleware(ILogProvider Log, CorsSecurityConfig secConfig) : IHttpMiddleware
+ {
+ private readonly FrozenSet<string> _corsAuthority = secConfig.AllowedCorsAuthority.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
+
+ public ValueTask<FileProcessArgs> ProcessAsync(HttpEntity entity)
+ {
+ //Check coors enabled
+ bool isCors = entity.Server.IsCors();
+ bool isCrossSite = entity.Server.IsCrossSite();
+
+ /*
+ * Deny/allow cross site/cors requests at the site-level
+ */
+ if (!secConfig.DenyCorsCons)
+ {
+ //Confirm the origin is allowed during cors connections
+ if (entity.Server.CrossOrigin && _corsAuthority.Count > 0)
+ {
+ //If the authority is not allowed, deny the connection
+ if (!_corsAuthority.Contains(entity.Server.Origin!.Authority))
+ {
+ Log.Debug("Denied a connection from a cross-origin site {s}, the origin was not whitelisted", entity.Server.Origin);
+ return ValueTask.FromResult(FileProcessArgs.Deny);
+ }
+ }
+
+ if (isCors)
+ {
+ //set the allow credentials header
+ entity.Server.Headers["Access-Control-Allow-Credentials"] = "true";
+
+ //If cross site flag is set, or the connection has cross origin flag set, set explicit origin
+ if (entity.Server.CrossOrigin || isCrossSite && entity.Server.Origin != null)
+ {
+ entity.Server.Headers["Access-Control-Allow-Origin"] = $"{entity.Server.RequestUri.Scheme}://{entity.Server.Origin!.Authority}";
+ //Add origin to the response vary header when setting cors origin
+ entity.Server.Headers.Append(HttpResponseHeader.Vary, "Origin");
+ }
+ }
+
+ //Add sec vary headers for cors enabled sites
+ entity.Server.Headers.Append(HttpResponseHeader.Vary, "Sec-Fetch-Dest,Sec-Fetch-Mode,Sec-Fetch-Site");
+ }
+ else if (isCors | isCrossSite)
+ {
+ Log.Verbose("Denied a cross-site/cors request from {con} because this site does not allow cross-site/cors requests", entity.TrustedRemoteIp);
+ return ValueTask.FromResult(FileProcessArgs.Deny);
+ }
+
+ //If user-navigation is set and method is get, make sure it does not contain object/embed
+ if (entity.Server.IsNavigation() && entity.Server.Method == HttpMethod.GET)
+ {
+ string? dest = entity.Server.Headers["sec-fetch-dest"];
+ if (dest != null && (dest.Contains("object", StringComparison.OrdinalIgnoreCase) || dest.Contains("embed", StringComparison.OrdinalIgnoreCase)))
+ {
+ Log.Debug("Denied a browser navigation request from {con} because it contained an object/embed", entity.TrustedRemoteIp);
+ return ValueTask.FromResult(FileProcessArgs.Deny);
+ }
+ }
+
+ //If the connection is a cross-site, then an origin header must be supplied
+ if (isCrossSite && entity.Server.Origin is null)
+ {
+ Log.Debug("Denied cross-site request because origin header was not supplied");
+ return ValueTask.FromResult(FileProcessArgs.Deny);
+ }
+
+ //If same origin is supplied, enforce origin header on post/options/put/patch
+ if (string.Equals("same-origin", entity.Server.Headers["Sec-Fetch-Site"], StringComparison.OrdinalIgnoreCase))
+ {
+ //If method is not get/head, then origin is required
+ if ((entity.Server.Method & (HttpMethod.GET | HttpMethod.HEAD)) == 0 && entity.Server.Origin == null)
+ {
+ Log.Debug("Denied same-origin POST/PUT... request because origin header was not supplied");
+ return ValueTask.FromResult(FileProcessArgs.Deny);
+ }
+ }
+
+ if(!IsSessionSecured(entity))
+ {
+ return ValueTask.FromResult(FileProcessArgs.Deny);
+ }
+
+ return ValueTask.FromResult(FileProcessArgs.Continue);
+ }
+
+ private bool IsSessionSecured(HttpEntity entity)
+ {
+ ref readonly SessionInfo session = ref entity.Session;
+
+ /*
+ * When sessions are created for connections that come from a different
+ * origin, their origin is stored for later.
+ *
+ * If the session was created from a different origin or the current connection
+ * is cross origin, then the origin must be allowed by the configuration
+ */
+
+ //No session loaded, nothing to check
+ if (!session.IsSet)
+ {
+ return true;
+ }
+
+ if (entity.Server.Origin is null)
+ {
+ return true;
+ }
+
+ if (session.IsNew || session.SessionType != SessionType.Web)
+ {
+ return true;
+ }
+
+ bool sameOrigin = string.Equals(
+ entity.Server.Origin.Authority,
+ session.SpecifiedOrigin?.Authority,
+ StringComparison.OrdinalIgnoreCase
+ );
+
+ if (sameOrigin || _corsAuthority.Contains(entity.Server.Origin.Authority))
+ {
+ return true;
+ }
+
+ Log.Debug("Denied connection from {0} because the user's origin {org} changed to {other} and is not whitelisted.",
+ entity.TrustedRemoteIp,
+ session.SpecifiedOrigin?.Authority,
+ entity.Server.Origin.Authority
+ );
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/apps/VNLib.WebServer/src/Middlewares/ConnectionLogMiddleware.cs b/apps/VNLib.WebServer/src/Middlewares/ConnectionLogMiddleware.cs
new file mode 100644
index 0000000..939fd0d
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Middlewares/ConnectionLogMiddleware.cs
@@ -0,0 +1,108 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: ConnectionLogMiddleware.cs
+*
+* ConnectionLogMiddleware.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+/*
+ * Provides an Nginx-style log of incoming connections.
+ */
+
+using System.Threading.Tasks;
+using System.Security.Authentication;
+
+using VNLib.Net.Http;
+using VNLib.Utils.Logging;
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.Middleware;
+
+namespace VNLib.WebServer.Middlewares
+{
+ internal sealed class ConnectionLogMiddleware(ILogProvider Log) : IHttpMiddleware
+ {
+ const string template = @"{ip} - {usr} [{local_time}] {tls} '{method} {url} {http_version}' {hostname} '{refer}' '{user_agent}' '{forwarded_for}'";
+
+ ///<inheritdoc/>
+ public ValueTask<FileProcessArgs> ProcessAsync(HttpEntity entity)
+ {
+ if (Log.IsEnabled(LogLevel.Information))
+ {
+ string userId = string.Empty;
+ if (entity.Session.IsSet)
+ {
+ userId = entity.Session.UserID;
+ }
+
+ Log.Information(template,
+ entity.TrustedRemoteIp,
+ userId,
+ entity.RequestedTimeUtc.ToLocalTime().ToString("dd/MMM/yyyy:HH:mm:ss zzz", null),
+ GetTlsInfo(entity),
+ entity.Server.Method,
+ entity.Server.RequestUri.PathAndQuery,
+ GetProtocolVersionString(entity),
+ entity.RequestedRoot.Hostname,
+ entity.Server.Referer,
+ entity.Server.UserAgent,
+ entity.Server.Headers["X-Forwarded-For"] ?? string.Empty
+ );
+ }
+
+ return ValueTask.FromResult(FileProcessArgs.Continue);
+ }
+
+ static string GetProtocolVersionString(HttpEntity entity)
+ {
+ return entity.Server.ProtocolVersion switch
+ {
+ HttpVersion.Http09 => "HTTP/0.9",
+ HttpVersion.Http1 => "HTTP/1.0",
+ HttpVersion.Http11 => "HTTP/1.1",
+ HttpVersion.Http2 => "HTTP/2.0",
+ HttpVersion.Http3 => "HTTP/3.0",
+ _ => "HTTP/1.1"
+ };
+ }
+
+ static string GetTlsInfo(HttpEntity entity)
+ {
+ ref readonly TransportSecurityInfo? secInfo = ref entity.Server.GetTransportSecurityInfo();
+
+ if(!secInfo.HasValue)
+ {
+ return string.Empty;
+ }
+
+#pragma warning disable CA5398, CA5397, SYSLIB0039 // Avoid hardcoded SslProtocols values
+
+ return secInfo.Value.SslProtocol switch
+ {
+ SslProtocols.Tls => "TLSv1.0",
+ SslProtocols.Tls11 => "TLSv1.1",
+ SslProtocols.Tls12 => "TLSv1.2",
+ SslProtocols.Tls13 => "TLSv1.3",
+ _ => "Unknown"
+ };
+
+#pragma warning restore CA5397, CA5398, SYSLIB0039 // Do not use deprecated SslProtocols values
+ }
+ }
+} \ No newline at end of file
diff --git a/apps/VNLib.WebServer/src/Middlewares/IpBlacklistMiddleware.cs b/apps/VNLib.WebServer/src/Middlewares/IpBlacklistMiddleware.cs
new file mode 100644
index 0000000..5959a95
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Middlewares/IpBlacklistMiddleware.cs
@@ -0,0 +1,49 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: WhitelistMiddleware.cs
+*
+* WhitelistMiddleware.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Net;
+using System.Threading.Tasks;
+using System.Collections.Frozen;
+
+using VNLib.Utils.Logging;
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.Middleware;
+
+namespace VNLib.WebServer.Middlewares
+{
+ [MiddlewareImpl(MiddlewareImplOptions.SecurityCritical)]
+ internal sealed class IpBlacklistMiddleware(ILogProvider Log, FrozenSet<IPAddress> Blacklist) : IHttpMiddleware
+ {
+ public ValueTask<FileProcessArgs> ProcessAsync(HttpEntity entity)
+ {
+ if (Blacklist.Contains(entity.TrustedRemoteIp))
+ {
+ Log.Verbose("Client {ip} is blacklisted, blocked", entity.TrustedRemoteIp);
+ return ValueTask.FromResult(FileProcessArgs.Deny);
+ }
+
+ return ValueTask.FromResult(FileProcessArgs.Continue);
+ }
+ }
+} \ No newline at end of file
diff --git a/apps/VNLib.WebServer/src/Middlewares/IpWhitelistMiddleware.cs b/apps/VNLib.WebServer/src/Middlewares/IpWhitelistMiddleware.cs
new file mode 100644
index 0000000..bda754d
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Middlewares/IpWhitelistMiddleware.cs
@@ -0,0 +1,53 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: WhitelistMiddleware.cs
+*
+* WhitelistMiddleware.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Net;
+using System.Threading.Tasks;
+using System.Collections.Frozen;
+
+using VNLib.Utils.Logging;
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.Middleware;
+
+namespace VNLib.WebServer.Middlewares
+{
+ /*
+ * Middelware that matches clients real ip addresses against a whitelist
+ * and blocks them if they are not on the list
+ */
+ [MiddlewareImpl(MiddlewareImplOptions.SecurityCritical)]
+ internal sealed class IpWhitelistMiddleware(ILogProvider Log, FrozenSet<IPAddress> WhiteList) : IHttpMiddleware
+ {
+ public ValueTask<FileProcessArgs> ProcessAsync(HttpEntity entity)
+ {
+ if (!WhiteList.Contains(entity.TrustedRemoteIp))
+ {
+ Log.Verbose("Client {ip} is not whitelisted, blocked", entity.TrustedRemoteIp);
+ return ValueTask.FromResult(FileProcessArgs.Deny);
+ }
+
+ return ValueTask.FromResult(FileProcessArgs.Continue);
+ }
+ }
+} \ No newline at end of file
diff --git a/apps/VNLib.WebServer/src/Middlewares/MainServerMiddlware.cs b/apps/VNLib.WebServer/src/Middlewares/MainServerMiddlware.cs
new file mode 100644
index 0000000..da7eb3d
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Middlewares/MainServerMiddlware.cs
@@ -0,0 +1,100 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: MainServerMiddlware.cs
+*
+* MainServerMiddlware.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Net;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+using VNLib.Net.Http;
+using VNLib.Utils.Logging;
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.Extensions;
+using VNLib.Plugins.Essentials.Middleware;
+
+namespace VNLib.WebServer.Middlewares
+{
+
+ /// <summary>
+ /// Provides required/essential server functionality as a middelware processor
+ /// </summary>
+ /// <param name="Log"></param>
+ /// <param name="VirtualHostOptions"></param>
+ internal sealed class MainServerMiddlware(ILogProvider Log, VirtualHostConfig VirtualHostOptions, bool forcePorts) : IHttpMiddleware
+ {
+ public ValueTask<FileProcessArgs> ProcessAsync(HttpEntity entity)
+ {
+ //Set special server header
+ VirtualHostOptions.TrySetSpecialHeader(entity.Server, SpecialHeaders.Server);
+
+ //Block websocket requests
+ if (entity.Server.IsWebSocketRequest)
+ {
+ Log.Verbose("Client {ip} made a websocket request", entity.TrustedRemoteIp);
+ }
+
+ ref readonly TransportSecurityInfo? tlsSecInfo = ref entity.Server.GetTransportSecurityInfo();
+
+ //Check transport security if set
+ if (tlsSecInfo.HasValue)
+ {
+
+ }
+
+ //If not behind upstream server, uri ports and server ports must match
+ bool enforcePortCheck = !entity.IsBehindDownStreamServer && forcePorts;
+
+ if (enforcePortCheck && !entity.Server.EnpointPortsMatch())
+ {
+ Log.Debug("Connection {ip} received on port {p} but the client host port did not match at {pp}",
+ entity.TrustedRemoteIp,
+ entity.Server.LocalEndpoint.Port,
+ entity.Server.RequestUri.Port
+ );
+
+ return ValueTask.FromResult(FileProcessArgs.Deny);
+ }
+
+ /*
+ * downstream server will handle the transport security,
+ * if the connection is not from an downstream server
+ * and is using transport security then we can specify HSTS
+ */
+ if (entity.IsSecure)
+ {
+ VirtualHostOptions.TrySetSpecialHeader(entity.Server, SpecialHeaders.Hsts);
+ }
+
+ //Add response headers from vh config
+ for (int i = 0; i < VirtualHostOptions.AdditionalHeaders.Length; i++)
+ {
+ //Get and append the client header value
+ ref KeyValuePair<string, string> header = ref VirtualHostOptions.AdditionalHeaders[i];
+
+ entity.Server.Headers.Append(header.Key, header.Value);
+ }
+
+ return ValueTask.FromResult(FileProcessArgs.Continue);
+ }
+ }
+} \ No newline at end of file
diff --git a/apps/VNLib.WebServer/src/Plugins/PluginAssemblyLoader.cs b/apps/VNLib.WebServer/src/Plugins/PluginAssemblyLoader.cs
new file mode 100644
index 0000000..b49c782
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Plugins/PluginAssemblyLoader.cs
@@ -0,0 +1,125 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: PluginAssemblyLoader.cs
+*
+* PluginAssemblyLoader.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Diagnostics;
+using System.Reflection;
+using System.Runtime.Loader;
+
+#if USE_MCMASTER
+using McMaster.NETCore.Plugins;
+#endif
+
+using VNLib.Plugins.Runtime;
+using VNLib.Utils.Resources;
+
+namespace VNLib.WebServer.Plugins
+{
+
+ internal static class PluginAsemblyLoading
+ {
+ public static IAssemblyLoader Create(IPluginAssemblyLoadConfig config)
+ {
+ return config.Unloadable ? new UnloadableAlc(config) : new ImmutableAl(config);
+ }
+
+ //Immutable assembly loader
+ internal sealed record class ImmutableAl(IPluginAssemblyLoadConfig Config) : IAssemblyLoader
+ {
+ private readonly AssemblyLoadContext ctx = new(Config.AssemblyFile, Config.Unloadable);
+ private ManagedLibrary ml = null!;
+
+ ///<inheritdoc/>
+ public Assembly GetAssembly() => ml.Assembly;
+
+ ///<inheritdoc/>
+ public void Load() => ml = ManagedLibrary.LoadManagedAssembly(Config.AssemblyFile, ctx);
+
+ ///<inheritdoc/>
+ public void Unload() => Debug.Fail("Unload was called on an immutable assembly loader");
+
+ public void Dispose() { }
+ }
+
+ internal sealed record class UnloadableAlc(IPluginAssemblyLoadConfig Config) : IAssemblyLoader
+ {
+
+#if USE_MCMASTER
+ private readonly PluginLoader _loader = new(new(Config.AssemblyFile)
+ {
+ PreferSharedTypes = true,
+ IsUnloadable = Config.Unloadable,
+ LoadInMemory = Config.Unloadable
+ });
+
+ ///<inheritdoc/>
+ public Assembly GetAssembly() => _loader.LoadDefaultAssembly();
+
+ ///<inheritdoc/>
+ public void Load() => _loader.Load();
+
+ ///<inheritdoc/>
+ public void Unload()
+ {
+ if (Config.Unloadable)
+ {
+ //Cleanup old loader, dont invoke GC because runtime will handle it
+ _loader.Destroy(false);
+ //ctx.Unload();
+ //ml = null!;
+
+ //Init new load context with the same name
+ //ctx = new AssemblyLoadContext(Config.AssemblyFile, Config.Unloadable);
+ }
+ }
+
+ public void Dispose() => Unload();
+#else
+
+ private AssemblyLoadContext ctx = null!;
+ private ManagedLibrary ml = null!;
+
+ public void Dispose() => Unload();
+
+ public Assembly GetAssembly() => ml.Assembly;
+
+ public void Load()
+ {
+ Debug.Assert(Config.Unloadable, "Assumed unloadable context when using UnloadableAlc");
+
+ //A new load context is created for each load
+ ctx = new(Config.AssemblyFile, Config.Unloadable);
+ ml = ManagedLibrary.LoadManagedAssembly(Config.AssemblyFile, ctx);
+ }
+
+ public void Unload()
+ {
+ ctx.Unload();
+ ml = null!;
+ }
+#endif
+
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/RuntimeLoading/ProcessArguments.cs b/apps/VNLib.WebServer/src/RuntimeLoading/ProcessArguments.cs
new file mode 100644
index 0000000..b6db2e6
--- /dev/null
+++ b/apps/VNLib.WebServer/src/RuntimeLoading/ProcessArguments.cs
@@ -0,0 +1,40 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: ProcessArguments.cs
+*
+* ProcessArguments.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Collections.Generic;
+
+using VNLib.Utils;
+
+namespace VNLib.WebServer.RuntimeLoading
+{
+ internal sealed class ProcessArguments(IEnumerable<string> args) : ArgumentList(args)
+ {
+ public bool Verbose => HasArgument("-v") || HasArgument("--verbose");
+ public bool Debug => HasArgument("-d") || HasArgument("--debug");
+ public bool Silent => HasArgument("-s") || HasArgument("--silent");
+ public bool DoubleVerbose => Verbose && HasArgument("-vv");
+ public bool LogHttp => HasArgument("--log-http");
+ public bool ZeroAllocations => HasArgument("--zero-alloc");
+ }
+}
diff --git a/apps/VNLib.WebServer/src/RuntimeLoading/ServerLogBuilder.cs b/apps/VNLib.WebServer/src/RuntimeLoading/ServerLogBuilder.cs
new file mode 100644
index 0000000..4c55258
--- /dev/null
+++ b/apps/VNLib.WebServer/src/RuntimeLoading/ServerLogBuilder.cs
@@ -0,0 +1,154 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: ServerLogBuilder.cs
+*
+* ServerLogBuilder.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Collections.Generic;
+
+using Serilog;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.WebServer.RuntimeLoading
+{
+ internal sealed class ServerLogBuilder
+ {
+ public LoggerConfiguration SysLogConfig { get; }
+ public LoggerConfiguration AppLogConfig { get; }
+ public LoggerConfiguration? DebugConfig { get; }
+
+ public ServerLogBuilder()
+ {
+ AppLogConfig = new();
+ SysLogConfig = new();
+ }
+
+ public ServerLogBuilder BuildForConsole(ProcessArguments args)
+ {
+ InitConsoleLog(args, AppLogConfig, "Application");
+ InitConsoleLog(args, SysLogConfig, "System");
+ return this;
+ }
+
+ public ServerLogBuilder BuildFromConfig(JsonElement logEl)
+ {
+ InitSingleLog(logEl, "app_log", "Application", AppLogConfig);
+ InitSingleLog(logEl, "sys_log", "System", SysLogConfig);
+ return this;
+ }
+
+ public ServerLogger GetLogger()
+ {
+ //Return logger
+ return new (
+ new(AppLogConfig),
+ new(SysLogConfig),
+ DebugConfig == null ? null : new(DebugConfig)
+ );
+ }
+
+ private static void InitConsoleLog(ProcessArguments args, LoggerConfiguration conf, string logName)
+ {
+ //Set verbosity level, defaul to informational
+ if (args.Verbose)
+ {
+ conf.MinimumLevel.Verbose();
+ }
+ else if (args.Debug)
+ {
+ conf.MinimumLevel.Debug();
+ }
+ else
+ {
+ conf.MinimumLevel.Information();
+ }
+
+ //Setup loggers to write to console unless the -s silent arg is set
+ if (!args.Silent)
+ {
+ string template = $"{{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}} [{{Level:u3}}] {logName} {{Message:lj}}{{NewLine}}{{Exception}}";
+ _ = conf.WriteTo.Console(outputTemplate: template);
+ }
+ }
+
+ private static void InitSingleLog(JsonElement el, string elPath, string logName, LoggerConfiguration logConfig)
+ {
+ string? filePath = null;
+ string? template = null;
+
+ TimeSpan flushInterval = TimeSpan.FromSeconds(10);
+ int retainedLogs = 31;
+ //Default to 500mb log file size
+ int fileSizeLimit = 500 * 1000 * 1024;
+ RollingInterval interval = RollingInterval.Infinite;
+
+ //try to get the log config object
+ if (el.TryGetProperty(elPath, out JsonElement logEl))
+ {
+ IReadOnlyDictionary<string, JsonElement> conf = logEl.EnumerateObject().ToDictionary(static s => s.Name, static s => s.Value);
+
+ filePath = conf.GetPropString("path");
+ template = conf.GetPropString("template");
+
+ if (conf.TryGetValue("flush_sec", out JsonElement flushEl))
+ {
+ flushInterval = flushEl.GetTimeSpan(TimeParseType.Seconds);
+ }
+
+ if (conf.TryGetValue("retained_files", out JsonElement retainedEl))
+ {
+ retainedLogs = retainedEl.GetInt32();
+ }
+
+ if (conf.TryGetValue("file_size_limit", out JsonElement sizeEl))
+ {
+ fileSizeLimit = sizeEl.GetInt32();
+ }
+
+ if (conf.TryGetValue("interval", out JsonElement intervalEl))
+ {
+ interval = Enum.Parse<RollingInterval>(intervalEl.GetString()!, true);
+ }
+
+ //Set default objects
+ filePath ??= Path.Combine(Environment.CurrentDirectory, $"{elPath}.txt");
+ template ??= $"{{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}} [{{Level:u3}}] {logName} {{Message:lj}}{{NewLine}}{{Exception}}";
+
+ //Configure the log file writer
+ logConfig.WriteTo.File(filePath,
+ buffered: true,
+ retainedFileCountLimit: retainedLogs,
+ formatProvider:null,
+ fileSizeLimitBytes: fileSizeLimit,
+ rollingInterval: interval,
+ outputTemplate: template,
+ flushToDiskInterval: flushInterval);
+ }
+
+ //If the log element is not specified in config, do not write log files
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/RuntimeLoading/ServerLogger.cs b/apps/VNLib.WebServer/src/RuntimeLoading/ServerLogger.cs
new file mode 100644
index 0000000..7b93bfe
--- /dev/null
+++ b/apps/VNLib.WebServer/src/RuntimeLoading/ServerLogger.cs
@@ -0,0 +1,45 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: ServerLogger.cs
+*
+* ServerLogger.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using VNLib.Utils;
+
+namespace VNLib.WebServer.RuntimeLoading
+{
+ internal sealed class ServerLogger(VLogProvider applog, VLogProvider syslog, VLogProvider? debuglog) : VnDisposeable
+ {
+
+ public VLogProvider AppLog { get; } = applog;
+
+ public VLogProvider SysLog { get; } = syslog;
+
+ public VLogProvider? DebugLog { get; } = debuglog;
+
+ protected override void Free()
+ {
+ AppLog.Dispose();
+ SysLog.Dispose();
+ DebugLog?.Dispose();
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Transport/HostAwareServerSslOptions.cs b/apps/VNLib.WebServer/src/Transport/HostAwareServerSslOptions.cs
new file mode 100644
index 0000000..ed97830
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Transport/HostAwareServerSslOptions.cs
@@ -0,0 +1,108 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: HostAwareServerSslOptions.cs
+*
+* HostAwareServerSslOptions.cs is part of VNLib.WebServer which is part
+* of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Net.Security;
+using System.Collections.Generic;
+using System.Security.Authentication;
+using System.Security.Cryptography.X509Certificates;
+
+using VNLib.WebServer.Config;
+using VNLib.WebServer.Config.Model;
+
+namespace VNLib.WebServer.Transport
+{
+
+ internal sealed class HostAwareServerSslOptions : SslServerAuthenticationOptions
+ {
+ //TODO programatically setup ssl protocols, but for now we only use HTTP/1.1 so this can be hard-coded
+ internal static readonly List<SslApplicationProtocol> SslAppProtocols = new()
+ {
+ SslApplicationProtocol.Http11,
+ //SslApplicationProtocol.Http2,
+ };
+
+ private readonly bool _clientCertRequired;
+ private readonly X509Certificate _cert;
+
+ private readonly SslPolicyErrors _errorLevel;
+
+ public HostAwareServerSslOptions(TransportInterface iFace)
+ {
+ ArgumentNullException.ThrowIfNull(iFace);
+ Validate.Assert(iFace.Ssl, "An interface was selected that does not have SSL enabled. This is likely a bug");
+
+ _clientCertRequired = iFace.ClientCertRequired;
+ _cert = iFace.LoadCertificate()!;
+
+ /*
+ * If client certificates are required, then no policy errors are allowed
+ * and the certificate must be requested from the user. Otherwise, no errors
+ * except missing certificates are allowed
+ */
+ _errorLevel = _clientCertRequired
+ ? SslPolicyErrors.None
+ : SslPolicyErrors.RemoteCertificateNotAvailable;
+
+ //Set validation callback
+ RemoteCertificateValidationCallback = OnRemoteCertVerification;
+ ServerCertificateSelectionCallback = OnGetCertificatForHost;
+
+ ConfigureBaseDefaults(iFace.UseOsCiphers);
+ }
+
+ private void ConfigureBaseDefaults(bool doNotForceProtocols)
+ {
+ //Eventually when HTTP2 is supported, we can select the ssl version to match
+ ApplicationProtocols = SslAppProtocols;
+
+ AllowRenegotiation = false;
+ EncryptionPolicy = EncryptionPolicy.RequireEncryption;
+
+ //Allow user to disable forced protocols and let the os decide
+ EnabledSslProtocols = doNotForceProtocols
+ ? SslProtocols.None
+ : SslProtocols.Tls12 | SslProtocols.Tls13;
+ }
+
+ private bool OnRemoteCertVerification(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors)
+ {
+ /*
+ * Since certificates are loaded at the interface level, an interface is defined at the virtual host level
+ * and can only accept one certificate per virtual host. So SNI is not useful here since certificates are
+ * verified at the interface level.
+ */
+
+ return _errorLevel == sslPolicyErrors;
+ }
+
+ /*
+ * Callback for getting the certificate from a hostname
+ *
+ * Always used the certificate defined at the interface level
+ */
+ private X509Certificate OnGetCertificatForHost(object sender, string? hostName) => _cert;
+
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Transport/SslTcpTransportContext.cs b/apps/VNLib.WebServer/src/Transport/SslTcpTransportContext.cs
new file mode 100644
index 0000000..8e3c2fd
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Transport/SslTcpTransportContext.cs
@@ -0,0 +1,103 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: SslTcpTransportContext.cs
+*
+* SslTcpTransportContext.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+
+using System.Net.Security;
+using System.Threading.Tasks;
+using System.Runtime.CompilerServices;
+
+using VNLib.Net.Http;
+using VNLib.Net.Transport.Tcp;
+
+namespace VNLib.WebServer.Transport
+{
+ internal sealed class SslTcpTransportContext(ITcpListner server, ITcpConnectionDescriptor descriptor, SslStream stream)
+ : TcpTransportContext(server, descriptor, stream)
+ {
+ private TransportSecurityInfo? _securityInfo;
+ private readonly SslStream _baseStream = stream;
+
+ ///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public async override ValueTask CloseConnectionAsync()
+ {
+ try
+ {
+ //Shutdown the ssl stream before cleaning up the connection
+ await _baseStream.ShutdownAsync();
+ await _connectionStream.DisposeAsync();
+ }
+ finally
+ {
+ //Always close the underlying connection
+ await base.CloseConnectionAsync();
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override ref readonly TransportSecurityInfo? GetSecurityInfo()
+ {
+ //Value has not been loaded yet, so lazy load it
+ if (!_securityInfo.HasValue)
+ {
+ //Create sec info from the ssl stream
+ GetSecInfo(ref _securityInfo, _baseStream);
+ }
+
+ return ref _securityInfo;
+ }
+
+
+ //Lazy load sec info
+ private static void GetSecInfo(ref TransportSecurityInfo? tsi, SslStream ssl)
+ {
+ //Build sec info
+ tsi = new()
+ {
+ SslProtocol = ssl.SslProtocol,
+ HashAlgorithm = ssl.HashAlgorithm,
+ CipherAlgorithm = ssl.CipherAlgorithm,
+
+ HashStrength = ssl.HashStrength,
+ CipherStrength = ssl.CipherStrength,
+
+ IsSigned = ssl.IsSigned,
+ IsEncrypted = ssl.IsEncrypted,
+ IsAuthenticated = ssl.IsAuthenticated,
+ IsMutuallyAuthenticated = ssl.IsMutuallyAuthenticated,
+ CheckCertRevocationStatus = ssl.CheckCertRevocationStatus,
+
+ KeyExchangeStrength = ssl.KeyExchangeStrength,
+ KeyExchangeAlgorithm = ssl.KeyExchangeAlgorithm,
+
+ LocalCertificate = ssl.LocalCertificate,
+ RemoteCertificate = ssl.RemoteCertificate,
+
+ TransportContext = ssl.TransportContext,
+ NegotiatedCipherSuite = ssl.NegotiatedCipherSuite,
+ NegotiatedApplicationProtocol = ssl.NegotiatedApplicationProtocol,
+ };
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Transport/TcpServerLoader.cs b/apps/VNLib.WebServer/src/Transport/TcpServerLoader.cs
new file mode 100644
index 0000000..12ad52f
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Transport/TcpServerLoader.cs
@@ -0,0 +1,221 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: TcpServerLoader.cs
+*
+* TcpServerLoader.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Text.Json;
+using System.Net.Sockets;
+using System.Collections.Generic;
+
+using VNLib.Utils.Logging;
+using VNLib.Utils.Resources;
+using VNLib.Net.Http;
+using VNLib.Net.Transport.Tcp;
+
+using VNLib.WebServer.Config;
+using VNLib.WebServer.RuntimeLoading;
+using VNLib.WebServer.Config.Model;
+
+using VNLib.Plugins.Essentials.ServiceStack;
+using VNLib.Plugins.Essentials.ServiceStack.Construction;
+
+namespace VNLib.WebServer.Transport
+{
+ internal sealed class TcpServerLoader(IServerConfig hostConfig, ProcessArguments args, ILogProvider tcpLogger)
+ {
+ const int CacheQuotaDefault = 0; //Disable cache quota by default, allows unlimited cache
+
+ const string TransportLogTemplate =
+@"Interface (TCP/IP): {iface} RX: {rx} TX: {tx} TLS: {tls} threads: {threads} max-cons: {max} Keepalive: {keepalive}";
+
+ private readonly LazyInitializer<TcpConfigJson> _conf = new(() =>
+ {
+ JsonElement rootElement = hostConfig.GetDocumentRoot();
+
+ if (rootElement.TryGetProperty(Entry.TCP_CONF_PROP_NAME, out JsonElement tcpEl))
+ {
+ return tcpEl.DeserializeElement<TcpConfigJson>()!;
+ }
+
+ return new TcpConfigJson();
+ });
+
+ private readonly bool UseInlineScheduler = args.HasArgument("--inline-scheduler");
+ private readonly string? ThreadCountArg = args.GetArgument("-t") ?? args.GetArgument("--threads");
+ private readonly bool EnableTransportLogging = args.HasArgument("--log-transport");
+ private readonly bool NoReuseSocket = args.HasArgument("--no-reuse-socket");
+ private readonly bool ReuseAddress = args.HasArgument("--reuse-address");
+
+ /// <summary>
+ /// The user confiugred TCP transmission buffer size
+ /// </summary>
+ public int TcpTxBufferSize => _conf.Instance.TcpSendBufferSize;
+
+ /// <summary>
+ /// The user-conifuigred TCP receive buffer size
+ /// </summary>
+ public int TcpRxBufferSize => _conf.Instance.TcpRecvBufferSize;
+
+ public HttpTransportMapping[] ReduceBindingsForGroups(IReadOnlyCollection<ServiceGroup> groups)
+ {
+ /*
+ * All transports can be reduced by their endpoints to reduce the number of
+ * TCP server instances that need to be created.
+ *
+ * The following code attempts to reorder the many-to-many set mapping of
+ * transports to virtual hosts into a set of one-to-many http bingings.
+ *
+ * Example:
+ *
+ * Virtual hosts Transports
+ * ex.com 0.0.0.0:80 (no ssl)
+ * ex.com 0.0.0.0:443 (ssl) (shared)
+ *
+ * ex1.com 192.168.1.6:80 (no ssl)
+ * ex1.com 0.0.0.0:443 (ssl) (shared)
+ */
+
+ Dictionary<TransportInterface, ITransportProvider> transportMap = groups
+ .SelectMany(static g => g.Hosts)
+ .Select(static c => (VirtualHostConfig)c.UserState!)
+ .SelectMany(static c => c.Transports)
+ .DistinctBy(static t => t.GetHashCode())
+ .ToDictionary(static k => k, GetProviderForInterface);
+
+ /*
+ * The following code groups virtual hosts that share the same transport
+ * interface and creates a new HttpTransportMapping instance for each
+ * group of shared interfaces, pulling the transport provider from the
+ * transport map above.
+ */
+
+ HttpTransportMapping[] bindings = groups
+ .SelectMany(static s => s.Hosts)
+ .SelectMany(static host =>
+ {
+ //The vhost config is stored as a user-object on the service host
+ VirtualHostConfig config = (VirtualHostConfig)host.UserState!;
+
+ return config.Transports.Select(iface => new OneToOneHostMapping(host, iface));
+ })
+ .GroupBy(static m => m.Interface.GetHashCode())
+ .Select(otoMap =>
+ {
+ IServiceHost[] sharedTransportHosts = otoMap
+ .Select(static m => m.Host)
+ .ToArray();
+
+ TransportInterface sharedInterface = otoMap.First().Interface;
+
+ //Find any duplicate hostnames that share the same transport interface and raise validation exception
+ string[] sharedHostErrors = sharedTransportHosts.GroupBy(static h => h.Processor.Hostname)
+ .Where(static g => g.Count() > 1)
+ .Select(duplicteGroup =>
+ {
+ string hostnames = string.Join(", ", duplicteGroup.Select(h => h.Processor.Hostname));
+
+ return $"Duplicate hostnames: {hostnames} share the same transport interface {sharedInterface}";
+ })
+ .ToArray();
+
+ //If any duplicate hostnames are found, raise a validation exception
+ if (sharedHostErrors.Length > 0)
+ {
+ throw new ServerConfigurationException(string.Join('\n', sharedHostErrors));
+ }
+
+ ITransportProvider mappedTransport = transportMap[sharedInterface];
+
+ return new HttpTransportMapping(sharedTransportHosts, mappedTransport);
+ })
+ .ToArray();
+
+ return bindings;
+ }
+
+ sealed record class OneToOneHostMapping(IServiceHost Host, TransportInterface Interface);
+
+ private ITransportProvider GetProviderForInterface(TransportInterface iface)
+ {
+ if (!uint.TryParse(ThreadCountArg, out uint threadCount))
+ {
+ threadCount = (uint)Environment.ProcessorCount;
+ }
+
+ TcpConfigJson baseConfig = _conf.Instance;
+ baseConfig.ValidateConfig();
+
+ TCPConfig tcpConf = new()
+ {
+ LocalEndPoint = iface.GetEndpoint(),
+ AcceptThreads = threadCount,
+ CacheQuota = CacheQuotaDefault,
+ Log = tcpLogger,
+ DebugTcpLog = EnableTransportLogging, //Only available in debug logging
+ BackLog = baseConfig.BackLog,
+ MaxConnections = baseConfig.MaxConnections,
+ TcpKeepAliveTime = baseConfig.TcpKeepAliveTime,
+ KeepaliveInterval = baseConfig.KeepaliveInterval,
+ MaxRecvBufferData = baseConfig.MaxRecvBufferData,
+ ReuseSocket = !NoReuseSocket, //Default to always reuse socket if allowed
+ OnSocketCreated = OnSocketConfiguring,
+ BufferPool = MemoryPoolManager.GetTcpPool(args.ZeroAllocations)
+ };
+
+ //Print warning message, since inline scheduler is an avanced feature
+ if (iface.Ssl && UseInlineScheduler)
+ {
+ tcpLogger.Debug("[WARN]: Inline scheduler is not available on server {server} when using TLS", tcpConf.LocalEndPoint);
+ }
+
+ tcpLogger.Verbose(TransportLogTemplate,
+ iface,
+ baseConfig.TcpRecvBufferSize,
+ baseConfig.TcpSendBufferSize,
+ iface.Ssl,
+ threadCount,
+ tcpConf.MaxConnections,
+ tcpConf.BackLog,
+ tcpConf.TcpKeepAliveTime > 0 ? $"{tcpConf.TcpKeepAliveTime} sec" : "Disabled"
+ );
+
+ //Init new tcp server with/without ssl
+ return iface.Ssl
+ ? TcpTransport.CreateServer(in tcpConf, ssl: new HostAwareServerSslOptions(iface))
+ : TcpTransport.CreateServer(in tcpConf, UseInlineScheduler);
+
+ }
+
+
+ private void OnSocketConfiguring(Socket serverSock)
+ {
+ TcpConfigJson baseConf = _conf.Instance;
+
+ serverSock.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, baseConf.NoDelay);
+ serverSock.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendBuffer, baseConf.TcpSendBufferSize);
+ serverSock.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveBuffer, baseConf.TcpRecvBufferSize);
+ serverSock.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, ReuseAddress);
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Transport/TcpTransport.cs b/apps/VNLib.WebServer/src/Transport/TcpTransport.cs
new file mode 100644
index 0000000..fd6e7d1
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Transport/TcpTransport.cs
@@ -0,0 +1,188 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: TcpTransport.cs
+*
+* TcpTransport.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+using System.IO.Pipelines;
+using System.Net.Security;
+using System.Threading.Tasks;
+using System.Security.Authentication;
+
+using VNLib.Net.Http;
+using VNLib.Net.Transport.Tcp;
+using VNLib.Utils.Logging;
+
+namespace VNLib.WebServer.Transport
+{
+ /// <summary>
+ /// Creates the TCP/HTTP translation layer providers
+ /// </summary>
+ internal static class TcpTransport
+ {
+ /// <summary>
+ /// Creates a new <see cref="ITransportProvider"/> that will listen for tcp connections
+ /// </summary>
+ /// <param name="config">The server configuration</param>
+ /// <param name="inlineScheduler">Use the inline pipeline scheduler</param>
+ /// <returns>The configured <see cref="ITransportProvider"/></returns>
+ public static ITransportProvider CreateServer(ref readonly TCPConfig config, bool inlineScheduler)
+ {
+ //Create tcp server
+ TcpServer server = new (config, CreateCustomPipeOptions(in config, inlineScheduler));
+ //Return provider
+ return new TcpTransportProvider(server);
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="ITransportProvider"/> that will listen for tcp connections
+ /// and use SSL
+ /// </summary>
+ /// <param name="config"></param>
+ /// <param name="ssl">The server authentication options</param>
+ /// <returns>The ssl configured transport context</returns>
+ public static ITransportProvider CreateServer(in TCPConfig config, SslServerAuthenticationOptions ssl)
+ {
+ /*
+ * SSL STREAM WORKAROUND
+ *
+ * The HttpServer impl calls Read() synchronously on the calling thread,
+ * it assumes that the call will make it synchronously to the underlying
+ * transport. SslStream calls ReadAsync() interally on the current
+ * synchronization context, which causes a deadlock... So the threadpool
+ * scheduler on the pipeline ensures that all continuations are run on the
+ * threadpool, which fixes this issue.
+ */
+
+ //Create tcp server
+ TcpServer server = new (config, CreateCustomPipeOptions(in config, false));
+ //Return provider
+ return new SslTcpTransportProvider(server, ssl);
+ }
+
+ private static PipeOptions CreateCustomPipeOptions(ref readonly TCPConfig config, bool inlineScheduler)
+ {
+ return new PipeOptions(
+ config.BufferPool,
+ //Noticable performance increase when using inline scheduler for reader (handles send operations)
+ readerScheduler: inlineScheduler ? PipeScheduler.Inline : PipeScheduler.ThreadPool,
+ writerScheduler: inlineScheduler ? PipeScheduler.Inline : PipeScheduler.ThreadPool,
+ pauseWriterThreshold: config.MaxRecvBufferData,
+ minimumSegmentSize: 8192,
+ useSynchronizationContext: false
+ );
+ }
+
+ /// <summary>
+ /// A TCP server transport provider class
+ /// </summary>
+ private class TcpTransportProvider(TcpServer Server) : ITransportProvider
+ {
+ protected ITcpListner? _listener;
+ protected CancellationTokenRegistration _reg;
+
+ ///<inheritdoc/>
+ void ITransportProvider.Start(CancellationToken stopToken)
+ {
+ //TEMPORARY (unless it works)
+ if(_listener is not null)
+ {
+ throw new InvalidOperationException("The server has already been started.");
+ }
+
+ //Start the server
+ _listener = Server.Listen();
+ _reg = stopToken.Register(_listener.Close, false);
+ }
+
+ ///<inheritdoc/>
+ public virtual async ValueTask<ITransportContext> AcceptAsync(CancellationToken cancellation)
+ {
+ //Wait for tcp event and wrap in ctx class
+ ITcpConnectionDescriptor descriptor = await _listener!.AcceptConnectionAsync(cancellation);
+
+ return new TcpTransportContext(_listener, descriptor, descriptor.GetStream());
+ }
+
+ ///<inheritdoc/>
+ public override string ToString() => $"{Server.Config.LocalEndPoint} tcp/ip";
+ }
+
+ private sealed class SslTcpTransportProvider(TcpServer Server, SslServerAuthenticationOptions AuthOptions)
+ : TcpTransportProvider(Server)
+ {
+ /*
+ * An SslStream may throw a win32 exception with HRESULT 0x80090327
+ * when processing a client certificate (I believe anyway) only
+ * an issue on some clients (browsers)
+ */
+
+ private const int UKNOWN_CERT_AUTH_HRESULT = unchecked((int)0x80090327);
+
+ /// <summary>
+ /// An invlaid frame size may happen if data is recieved on an open socket
+ /// but does not contain valid SSL handshake data
+ /// </summary>
+ private const int INVALID_FRAME_HRESULT = unchecked((int)0x80131501);
+
+ public override async ValueTask<ITransportContext> AcceptAsync(CancellationToken cancellation)
+ {
+ //Loop to handle ssl exceptions ourself
+ do
+ {
+ //Wait for tcp event and wrap in ctx class
+ ITcpConnectionDescriptor descriptor = await _listener.AcceptConnectionAsync(cancellation);
+
+ //Create ssl stream and auth
+ SslStream stream = new(descriptor.GetStream(), false);
+
+ try
+ {
+ //auth the new connection
+ await stream.AuthenticateAsServerAsync(AuthOptions, cancellation);
+ return new SslTcpTransportContext(_listener, descriptor, stream);
+ }
+ catch (AuthenticationException ae) when (ae.HResult == INVALID_FRAME_HRESULT)
+ {
+ Server.Config.Log.Debug("A TLS connection attempt was made but an invalid TLS frame was received");
+
+ await _listener.CloseConnectionAsync(descriptor, true);
+ await stream.DisposeAsync();
+
+ //continue listening loop
+ }
+ catch
+ {
+ await _listener.CloseConnectionAsync(descriptor, true);
+ await stream.DisposeAsync();
+ throw;
+ }
+ }
+ while (true);
+ }
+
+ ///<inheritdoc/>
+ public override string ToString() => $"{Server.Config.LocalEndPoint} tcp/ip (TLS enabled)";
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/Transport/TcpTransportContext.cs b/apps/VNLib.WebServer/src/Transport/TcpTransportContext.cs
new file mode 100644
index 0000000..2bb1b54
--- /dev/null
+++ b/apps/VNLib.WebServer/src/Transport/TcpTransportContext.cs
@@ -0,0 +1,93 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: TcpTransportContext.cs
+*
+* TcpTransportContext.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.IO;
+using System.Net;
+using System.Threading.Tasks;
+using System.Runtime.CompilerServices;
+
+using VNLib.Net.Http;
+using VNLib.Net.Transport.Tcp;
+
+namespace VNLib.WebServer.Transport
+{
+ /// <summary>
+ /// The TCP connection context
+ /// </summary>
+ internal class TcpTransportContext : ITransportContext
+ {
+ //Store static empty security info to pass in default case
+ private static readonly TransportSecurityInfo? EmptySecInfo;
+
+ protected readonly ITcpConnectionDescriptor _descriptor;
+
+ protected readonly Stream _connectionStream;
+ protected readonly IPEndPoint _localEndoint;
+ protected readonly IPEndPoint _remoteEndpoint;
+ protected readonly ITcpListner _server;
+
+ public TcpTransportContext(ITcpListner server, ITcpConnectionDescriptor descriptor, Stream stream)
+ {
+ _descriptor = descriptor;
+ _connectionStream = stream;
+ _server = server;
+ //Get the endpoints
+ descriptor.GetEndpoints(out _localEndoint, out _remoteEndpoint);
+ }
+
+ ///<inheritdoc/>
+ public virtual Stream ConnectionStream
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _connectionStream;
+ }
+
+ ///<inheritdoc/>
+ public virtual IPEndPoint LocalEndPoint
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _localEndoint;
+ }
+
+ ///<inheritdoc/>
+ public virtual IPEndPoint RemoteEndpoint
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _remoteEndpoint;
+ }
+
+ ///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public virtual async ValueTask CloseConnectionAsync()
+ {
+ //Close the stream before the descriptor
+ await _connectionStream.DisposeAsync();
+ await _server.CloseConnectionAsync(_descriptor, true);
+ }
+
+ //Ssl is not supported in this transport
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public virtual ref readonly TransportSecurityInfo? GetSecurityInfo() => ref EmptySecInfo;
+ }
+}
diff --git a/apps/VNLib.WebServer/src/VLogProvider.cs b/apps/VNLib.WebServer/src/VLogProvider.cs
new file mode 100644
index 0000000..d20437f
--- /dev/null
+++ b/apps/VNLib.WebServer/src/VLogProvider.cs
@@ -0,0 +1,84 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: VLogProvider.cs
+*
+* VLogProvider.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+using Serilog;
+using Serilog.Core;
+using Serilog.Events;
+
+using VNLib.Utils;
+using VNLib.Utils.Logging;
+
+namespace VNLib.WebServer
+{
+ internal sealed class VLogProvider : VnDisposeable, ILogProvider
+ {
+ private readonly Logger LogCore;
+
+ public VLogProvider(LoggerConfiguration config)
+ {
+ LogCore = config.CreateLogger();
+ }
+ public void Flush() { }
+
+ public object GetLogProvider() => LogCore;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool IsEnabled(LogLevel level) => LogCore.IsEnabled((LogEventLevel)level);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Write(LogLevel level, string value)
+ {
+ LogCore.Write((LogEventLevel)level, value);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Write(LogLevel level, Exception exception, string value = "")
+ {
+ LogCore.Write((LogEventLevel)level, exception, value);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Write(LogLevel level, string value, params object[] args)
+ {
+ LogCore.Write((LogEventLevel)level, value, args);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Write(LogLevel level, string value, params ValueType[] args)
+ {
+ //Serilog logger supports passing valuetypes to avoid boxing objects
+ if (LogCore.IsEnabled((LogEventLevel)level))
+ {
+ object[] ar = args.Select(a => (object)a).ToArray();
+ LogCore.Write((LogEventLevel)level, value, ar);
+ }
+ }
+
+ protected override void Free() => LogCore.Dispose();
+ }
+}
diff --git a/apps/VNLib.WebServer/src/VNLib.WebServer.csproj b/apps/VNLib.WebServer/src/VNLib.WebServer.csproj
new file mode 100644
index 0000000..37808b5
--- /dev/null
+++ b/apps/VNLib.WebServer/src/VNLib.WebServer.csproj
@@ -0,0 +1,88 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net8.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <AssemblyName>VNLib.WebServer</AssemblyName>
+ <RootNamespace>VNLib.WebServer</RootNamespace>
+ <ServerGarbageCollection>true</ServerGarbageCollection>
+ <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
+ <RetainVMGarbageCollection>true</RetainVMGarbageCollection>
+ <StartupObject>VNLib.WebServer.Entry</StartupObject>
+ </PropertyGroup>
+
+ <!-- Dotnet tool stuff -->
+ <PropertyGroup>
+ <PackAsTool>true</PackAsTool>
+ <ToolCommandName>VNLib.WebServer</ToolCommandName>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <AnalysisLevel Condition="'$(BuildingInsideVisualStudio)' == true">latest-all</AnalysisLevel>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <Authors>Vaughn Nugent</Authors>
+ <Company>Vaughn Nugent</Company>
+ <Description>A high performance, reference .NET 8 web/http server using VNLib.Core, with the VNLib.Plugins.Essentials web framework</Description>
+ <Copyright>Copyright © 2024 Vaughn Nugent</Copyright>
+ <Product>VNLib.Webserver</Product>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources/software/modules/vnlib.core</PackageProjectUrl>
+ <RepositoryUrl>https://github.com/VnUgE/VNLib.Core/tree/master/app/VNLib.Webserver/</RepositoryUrl>
+ <PackageReadmeFile>README.md</PackageReadmeFile>
+ <PackageLicenseFile>LICENSE</PackageLicenseFile>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ <DefineConstants>$(DefineConstants);USE_MCMASTER</DefineConstants>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ <DefineConstants>$(DefineConstants);USE_MCMASTER</DefineConstants>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <None Include="..\LICENSE">
+ <Pack>True</Pack>
+ <PackagePath>\</PackagePath>
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </None>
+ <None Include="..\README.md">
+ <Pack>True</Pack>
+ <PackagePath>\</PackagePath>
+ </None>
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Serilog" Version="4.0.1" />
+ <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
+ <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
+ <PackageReference Include="YamlDotNet" Version="16.0.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\lib\Plugins.Essentials\src\VNLib.Plugins.Essentials.csproj" />
+ <ProjectReference Include="..\..\..\lib\Net.Http\src\VNLib.Net.Http.csproj" />
+ <ProjectReference Include="..\..\..\lib\Net.Transport.SimpleTCP\src\VNLib.Net.Transport.SimpleTCP.csproj" />
+ <ProjectReference Include="..\..\..\lib\Plugins.Essentials.ServiceStack\src\VNLib.Plugins.Essentials.ServiceStack.csproj" />
+ <ProjectReference Include="..\..\..\third-party\DotNetCorePlugins\src\McMaster.NETCore.Plugins.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <None Update="sample.config.json">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
+</Project>
diff --git a/apps/VNLib.WebServer/src/VirtualHosts/FileCache.cs b/apps/VNLib.WebServer/src/VirtualHosts/FileCache.cs
new file mode 100644
index 0000000..de4efd9
--- /dev/null
+++ b/apps/VNLib.WebServer/src/VirtualHosts/FileCache.cs
@@ -0,0 +1,134 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: FileCache.cs
+*
+* FileCache.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Net;
+
+using VNLib.Net.Http;
+using VNLib.Utils;
+using VNLib.Utils.IO;
+
+namespace VNLib.WebServer
+{
+ /// <summary>
+ /// File the server will keep in memory and return to user when a specified error code is requested
+ /// </summary>
+ internal sealed class FileCache : VnDisposeable, IFSChangeHandler
+ {
+ private readonly string _filePath;
+
+ public readonly HttpStatusCode Code;
+
+ private Lazy<byte[]> _templateData;
+
+
+ /// <summary>
+ /// Catch an http error code and return the selected file to user
+ /// </summary>
+ /// <param name="code">Http status code to catch</param>
+ /// <param name="filePath">Path to file contating data to return to use on status code</param>
+ private FileCache(HttpStatusCode code, string filePath)
+ {
+ Code = code;
+ _filePath = filePath;
+ _templateData = new(LoadTemplateData);
+ }
+
+ private byte[] LoadTemplateData()
+ {
+ //Get file data as binary
+ return File.ReadAllBytes(_filePath);
+ }
+
+ /// <summary>
+ /// Gets a <see cref="IMemoryResponseReader"/> wrapper that may read a copy of the
+ /// file representation
+ /// </summary>
+ /// <returns>The <see cref="IMemoryResponseReader"/> wrapper around the file data</returns>
+ public IMemoryResponseReader GetReader() => new MemReader(_templateData.Value);
+
+
+ void IFSChangeHandler.OnFileChanged(FileSystemEventArgs e)
+ {
+ //Update lazy loader for new file update
+ _templateData = new(LoadTemplateData);
+ }
+
+ protected override void Free()
+ {
+ //Unsubscribe from file watcher
+ FileWatcher.Unsubscribe(_filePath, this);
+ }
+
+ /// <summary>
+ /// Create a new file cache for a specific error code
+ /// and begins listening for changes to the file
+ /// </summary>
+ /// <param name="code">The status code to produce the file for</param>
+ /// <param name="filePath">The path to the file to read</param>
+ /// <returns>The new <see cref="FileCache"/> instance if the file exists and is readable, null otherwise</returns>
+ public static FileCache? Create(HttpStatusCode code, string filePath)
+ {
+ //If the file does not exist, return null
+ if(!FileOperations.FileExists(filePath))
+ {
+ return null;
+ }
+
+ FileCache ff = new(code, filePath);
+
+ //Subscribe to file changes
+ FileWatcher.Subscribe(filePath, ff);
+
+ return ff;
+ }
+
+ private sealed class MemReader : IMemoryResponseReader
+ {
+ private readonly byte[] _memory;
+
+ private int _written;
+
+ public int Remaining { get; private set; }
+
+ internal MemReader(byte[] data)
+ {
+ //Store ref as memory
+ _memory = data;
+ Remaining = data.Length;
+ }
+
+ public void Advance(int written)
+ {
+ _written += written;
+ Remaining -= written;
+ }
+
+ void IMemoryResponseReader.Close() { }
+
+ ReadOnlyMemory<byte> IMemoryResponseReader.GetMemory() => _memory.AsMemory(_written, Remaining);
+ }
+ }
+} \ No newline at end of file
diff --git a/apps/VNLib.WebServer/src/VirtualHosts/IVirtualHostConfigBuilder.cs b/apps/VNLib.WebServer/src/VirtualHosts/IVirtualHostConfigBuilder.cs
new file mode 100644
index 0000000..7e837e7
--- /dev/null
+++ b/apps/VNLib.WebServer/src/VirtualHosts/IVirtualHostConfigBuilder.cs
@@ -0,0 +1,35 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: IVirtualHostConfigBuilder.cs
+*
+* IVirtualHostConfigBuilder.cs is part of VNLib.WebServer which is
+* part of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+namespace VNLib.WebServer
+{
+ internal interface IVirtualHostConfigBuilder
+ {
+ /// <summary>
+ /// Gets the base configuration for the virtualhost
+ /// </summary>
+ /// <returns></returns>
+ VirtualHostConfig GetBaseConfig();
+ }
+}
diff --git a/apps/VNLib.WebServer/src/VirtualHosts/JsonWebConfigBuilder.cs b/apps/VNLib.WebServer/src/VirtualHosts/JsonWebConfigBuilder.cs
new file mode 100644
index 0000000..820664c
--- /dev/null
+++ b/apps/VNLib.WebServer/src/VirtualHosts/JsonWebConfigBuilder.cs
@@ -0,0 +1,276 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: JsonWebConfigBuilder.cs
+*
+* JsonWebConfigBuilder.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Net;
+using System.Data;
+using System.Linq;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+
+using VNLib.WebServer.Config;
+using VNLib.WebServer.Config.Model;
+
+namespace VNLib.WebServer
+{
+
+ internal sealed partial class JsonWebConfigBuilder(VirtualHostServerConfig VhConfig, TimeSpan execTimeout, ILogProvider logger)
+ : IVirtualHostConfigBuilder
+ {
+ //Use pre-compiled default regex
+ private static readonly Regex DefaultRootRegex = MyRegex();
+
+ ///<inheritdoc/>
+ public VirtualHostConfig GetBaseConfig()
+ {
+ //Declare the vh config
+ return new()
+ {
+ //File root is required
+ RootDir = new(VhConfig.DirPath!),
+ LogProvider = logger,
+ ExecutionTimeout = execTimeout,
+ WhiteList = GetIpWhitelist(VhConfig),
+ DownStreamServers = GetDownStreamServers(VhConfig),
+ ExcludedExtensions = GetExlcudedExtensions(VhConfig),
+ DefaultFiles = GetDefaultFiles(VhConfig),
+ PathFilter = GetPathFilter(VhConfig),
+ CacheDefault = TimeSpan.FromSeconds(VhConfig.CacheDefaultTimeSeconds),
+ AdditionalHeaders = GetConfigHeaders(VhConfig),
+ SpecialHeaders = GetSpecialHeaders(VhConfig),
+ FailureFiles = GetFailureFiles(VhConfig),
+ FilePathCacheMaxAge = TimeSpan.MaxValue,
+ Hostnames = GetHostnames(VhConfig),
+ Transports = GetInterfaces(VhConfig),
+ BlackList = GetIpBlacklist(VhConfig),
+ };
+ }
+
+ private static string[] GetHostnames(VirtualHostServerConfig conf)
+ {
+ Validate.EnsureNotNull(conf.Hostnames, "Hostnames array was set to null, you must define at least one hostname");
+
+ foreach (string hostname in conf.Hostnames)
+ {
+ Validate.EnsureNotNull(hostname, "Hostname is null, all hostnames must be defined");
+ }
+
+ return conf.Hostnames;
+ }
+
+ private static TransportInterface[] GetInterfaces(VirtualHostServerConfig conf)
+ {
+ Validate.EnsureNotNull(conf.Interfaces, "Interfaces array was set to null, you must define at least one network interface");
+ Validate.Assert(conf.Interfaces.Length > 0, $"You must define at least one interface for host");
+
+ for(int i = 0; i < conf.Interfaces.Length; i++)
+ {
+ TransportInterface iFace = conf.Interfaces[i];
+
+ Validate.EnsureNotNull(iFace, $"Vrtual host interface [{i}] is undefined");
+
+ Validate.EnsureNotNull(iFace.Address, $"The interface IP address is required for interface [{i}]");
+ Validate.EnsureValidIp(iFace.Address, $"The interface IP address is invalid for interface [{i}]");
+ Validate.EnsureRange(iFace.Port, 1, 65535, "Interface port");
+ }
+
+ return conf.Interfaces;
+ }
+
+ private static Regex GetPathFilter(VirtualHostServerConfig conf)
+ {
+ //Allow site to define a regex filter pattern
+ return conf.PathFilter is not null ? new(conf.PathFilter!) : DefaultRootRegex;
+ }
+
+ private FrozenDictionary<HttpStatusCode, FileCache> GetFailureFiles(VirtualHostServerConfig conf)
+ {
+ //if a failure file array is specified, capure all files and
+ if (conf.ErrorFiles is null || conf.ErrorFiles.Length < 1)
+ {
+ return new Dictionary<HttpStatusCode, FileCache>().ToFrozenDictionary();
+ }
+
+ //Get the error files
+ IEnumerable<KeyValuePair<HttpStatusCode, string>> ffs = conf.ErrorFiles
+ .Select(static f => new KeyValuePair<HttpStatusCode, string>((HttpStatusCode)f.Code, f.Path!));
+
+ //Create the file cache dictionary
+ (HttpStatusCode, string, FileCache?)[] loadCache = ffs.Select(static kv =>
+ {
+ FileCache? cached = FileCache.Create(kv.Key, kv.Value);
+ return (kv.Key, kv.Value, cached);
+
+ }).ToArray();
+
+ //Only include files that exist and were loaded
+ int loadedFiles = loadCache
+ .Where(static loadCache => loadCache.Item3 != null)
+ .Count();
+
+ string[] notFoundFiles = loadCache
+ .Where(static loadCache => loadCache.Item3 == null)
+ .Select(static l => Path.GetFileName(l.Item2))
+ .ToArray();
+
+ if (notFoundFiles.Length > 0)
+ {
+ logger.Warn("Failed to load error files {files} for host {hosts}", notFoundFiles, conf.Hostnames);
+ }
+
+ //init frozen dictionary from valid cached files
+ return loadCache
+ .Where(static kv => kv.Item3 != null)
+ .ToDictionary(static kv => kv.Item1, static kv => kv.Item3!)
+ .ToFrozenDictionary();
+ }
+
+ private static FrozenSet<IPAddress> GetDownStreamServers(VirtualHostServerConfig conf)
+ {
+ //Find downstream servers
+ HashSet<IPAddress>? downstreamServers = null;
+
+ //See if element is set
+ if (conf.DownstreamServers is not null)
+ {
+ //hash addresses, make is distinct
+ downstreamServers = conf.DownstreamServers
+ .Where(static addr => !string.IsNullOrWhiteSpace(addr))
+ .Select(static addr => IPAddress.Parse(addr))
+ .Distinct()
+ .ToHashSet();
+ }
+
+ return (downstreamServers ?? []).ToFrozenSet();
+ }
+
+ private static FrozenSet<IPAddress>? GetIpWhitelist(VirtualHostServerConfig conf)
+ {
+ if(conf.Whitelist is null)
+ {
+ return null;
+ }
+
+ //See if whitelist is defined, if so, get a distinct list of addresses
+ return conf.Whitelist
+ .Where(static addr => !string.IsNullOrWhiteSpace(addr))
+ .Select(static addr => IPAddress.Parse(addr))
+ .Distinct()
+ .ToHashSet()
+ .ToFrozenSet();
+ }
+
+ private static FrozenSet<IPAddress>? GetIpBlacklist(VirtualHostServerConfig conf)
+ {
+ if (conf.Blacklist is null)
+ {
+ return null;
+ }
+
+ //See if whitelist is defined, if so, get a distinct list of addresses
+ return conf.Blacklist
+ .Where(static addr => !string.IsNullOrWhiteSpace(addr))
+ .Select(static addr => IPAddress.Parse(addr))
+ .Distinct()
+ .ToHashSet()
+ .ToFrozenSet();
+ }
+
+ private static FrozenSet<string> GetExlcudedExtensions(VirtualHostServerConfig conf)
+ {
+ //Get exlucded/denied extensions from config, ignore null strings
+ if (conf.DenyExtensions is not null)
+ {
+ return conf.DenyExtensions
+ .Where(static s => !string.IsNullOrWhiteSpace(s))
+ .Distinct()
+ .ToHashSet()
+ .ToFrozenSet(StringComparer.OrdinalIgnoreCase);
+ }
+ else
+ {
+ return new HashSet<string>().ToFrozenSet();
+ }
+ }
+
+ private static IReadOnlyCollection<string> GetDefaultFiles(VirtualHostServerConfig conf)
+ {
+ if(conf.DefaultFiles is null)
+ {
+ return Array.Empty<string>();
+ }
+
+ //Get blocked extensions for the root
+ return conf.DefaultFiles
+ .Where(static s => !string.IsNullOrWhiteSpace(s))
+ .Distinct()
+ .ToList();
+ }
+
+ private static KeyValuePair<string, string>[] GetConfigHeaders(VirtualHostServerConfig conf)
+ {
+ if (conf.Headers is null)
+ {
+ return [];
+ }
+
+ //Enumerate kv headers
+ return conf.Headers
+ //Ignore empty keys or values
+ .Where(static p => !string.IsNullOrWhiteSpace(p.Key) && string.IsNullOrWhiteSpace(p.Value))
+ //Exclude special headers
+ .Where(static p => !SpecialHeaders.SpecialHeader.Contains(p.Key, StringComparer.OrdinalIgnoreCase))
+ .Select(static p => new KeyValuePair<string, string>(p.Key!, p.Value))
+ .ToArray();
+ }
+
+ private static FrozenDictionary<string, string> GetSpecialHeaders(VirtualHostServerConfig conf)
+ {
+ //get the headers array property
+ if (conf.Headers is null)
+ {
+ return new Dictionary<string, string>().ToFrozenDictionary();
+ }
+
+ //Enumerate kv header
+ return conf.Headers
+ //Ignore empty keys or values
+ .Where(static p => !string.IsNullOrWhiteSpace(p.Key) && !string.IsNullOrWhiteSpace(p.Value))
+ //Only include special headers
+ .Where(static p => SpecialHeaders.SpecialHeader.Contains(p.Key, StringComparer.OrdinalIgnoreCase))
+ //Create the special dictionary
+ .ToDictionary(static k => k.Key, static k => k.Value, StringComparer.OrdinalIgnoreCase)
+ .ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
+ }
+
+
+ [GeneratedRegex(@"(\/\.\.)|(\\\.\.)|[\[\]^*<>|`~'\n\r\t\n]|(\s$)|^(\s)", RegexOptions.Compiled)]
+ private static partial Regex MyRegex();
+ }
+}
diff --git a/apps/VNLib.WebServer/src/VirtualHosts/SpecialHeaders.cs b/apps/VNLib.WebServer/src/VirtualHosts/SpecialHeaders.cs
new file mode 100644
index 0000000..a4a797d
--- /dev/null
+++ b/apps/VNLib.WebServer/src/VirtualHosts/SpecialHeaders.cs
@@ -0,0 +1,71 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: SpecialHeaders.cs
+*
+* SpecialHeaders.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Runtime.CompilerServices;
+
+using VNLib.Net.Http;
+
+namespace VNLib.WebServer
+{
+ /// <summary>
+ /// Contains constants for internal/special headers by their name
+ /// </summary>
+ internal static class SpecialHeaders
+ {
+ public const string ContentSecPolicy = "Content-Security-Policy";
+ public const string XssProtection = "X-XSS-Protection";
+ public const string XContentOption = "X-Content-Type-Options";
+ public const string Hsts = "Strict-Transport-Security";
+ public const string Server = "Server";
+
+ /// <summary>
+ /// An array of the special headers to quickly compare against
+ /// </summary>
+ public static string[] SpecialHeader =
+ {
+ ContentSecPolicy,
+ XssProtection,
+ XContentOption,
+ Hsts,
+ Server
+ };
+
+ /// <summary>
+ /// Appends the special header by the given name, if it is present
+ /// in the current configruation's special headers
+ /// </summary>
+ /// <param name="config"></param>
+ /// <param name="server">The connection to set the response headers on</param>
+ /// <param name="headerName">The name of the special header to get</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void TrySetSpecialHeader(this VirtualHostConfig config, IConnectionInfo server, string headerName)
+ {
+ //Try to get the special header value,
+ if(config.SpecialHeaders.TryGetValue(headerName, out string? headerValue))
+ {
+ server.Headers.Append(headerName, headerValue);
+ }
+ }
+ }
+}
diff --git a/apps/VNLib.WebServer/src/VirtualHosts/VirtualHostConfig.cs b/apps/VNLib.WebServer/src/VirtualHosts/VirtualHostConfig.cs
new file mode 100644
index 0000000..8af58aa
--- /dev/null
+++ b/apps/VNLib.WebServer/src/VirtualHosts/VirtualHostConfig.cs
@@ -0,0 +1,101 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: VirtualHostConfig.cs
+*
+* VirtualHostConfig.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Net;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.ServiceStack.Construction;
+
+using VNLib.WebServer.Config.Model;
+
+namespace VNLib.WebServer
+{
+ /// <summary>
+ /// Implementation of <see cref="IEpProcessingOptions"/>
+ /// with <see cref="VirtualHostHooks"/> extra processing options
+ /// </summary>
+ internal sealed class VirtualHostConfig : VirtualHostConfiguration, IEpProcessingOptions
+ {
+ public VirtualHostConfig()
+ {
+ //Update file attributes
+ AllowedAttributes = FileAttributes.Archive | FileAttributes.Compressed | FileAttributes.Normal | FileAttributes.ReadOnly;
+ DissallowedAttributes = FileAttributes.Device
+ | FileAttributes.Directory
+ | FileAttributes.Encrypted
+ | FileAttributes.Hidden
+ | FileAttributes.IntegrityStream
+ | FileAttributes.Offline
+ | FileAttributes.ReparsePoint
+ | FileAttributes.System;
+ }
+
+ /// <summary>
+ /// A regex filter instance to filter incoming filesystem paths
+ /// </summary>
+ public Regex? PathFilter { get; init; }
+
+ /// <summary>
+ /// The default response entity cache value
+ /// </summary>
+ public required TimeSpan CacheDefault { get; init; }
+
+ /// <summary>
+ /// A collection of in-memory files to send in response to processing error
+ /// codes.
+ /// </summary>
+ public FrozenDictionary<HttpStatusCode, FileCache> FailureFiles { get; init; } = new Dictionary<HttpStatusCode, FileCache>().ToFrozenDictionary();
+
+ /// <summary>
+ /// Allows config to specify contant additional headers
+ /// </summary>
+ public KeyValuePair<string, string>[] AdditionalHeaders { get; init; } = Array.Empty<KeyValuePair<string, string>>();
+
+ /// <summary>
+ /// Contains internal headers used for specific purposes, cherrypicked from the config headers
+ /// </summary>
+ public FrozenDictionary<string, string> SpecialHeaders { get; init; } = new Dictionary<string, string>().ToFrozenDictionary();
+
+ /// <summary>
+ /// The array of interfaces the host wishes to listen on
+ /// </summary>
+ internal required TransportInterface[] Transports { get; init; }
+
+ /// <summary>
+ /// An optional whitelist set of ipaddresses that are allowed to make connections to this site
+ /// </summary>
+ internal required FrozenSet<IPAddress>? WhiteList { get; init; }
+
+ /// <summary>
+ /// An optional blacklist set of ipaddresses that are not allowed to make connections to this site
+ /// </summary>
+ internal required FrozenSet<IPAddress>? BlackList { get; init; }
+
+ }
+}
diff --git a/apps/VNLib.WebServer/src/VirtualHosts/VirtualHostHooks.cs b/apps/VNLib.WebServer/src/VirtualHosts/VirtualHostHooks.cs
new file mode 100644
index 0000000..e61b0a0
--- /dev/null
+++ b/apps/VNLib.WebServer/src/VirtualHosts/VirtualHostHooks.cs
@@ -0,0 +1,231 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.WebServer
+* File: VirtualHostHooks.cs
+*
+* VirtualHostHooks.cs is part of VNLib.WebServer which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.WebServer is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.WebServer 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
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Net;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+
+using VNLib.Net.Http;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.Extensions;
+using VNLib.Plugins.Essentials.ServiceStack.Construction;
+
+namespace VNLib.WebServer
+{
+
+ internal sealed class VirtualHostHooks(VirtualHostConfig config) : IVirtualHostHooks
+ {
+ private const int FILE_PATH_BUILDER_BUFFER_SIZE = 4096;
+
+ private static readonly string CultreInfo = CultureInfo.InstalledUICulture.Name;
+
+ private readonly string DefaultCacheString = HttpHelpers.GetCacheString(CacheType.Public, (int)config.CacheDefault.TotalSeconds);
+
+ public bool ErrorHandler(HttpStatusCode errorCode, IHttpEvent ev)
+ {
+ //Make sure the connection accepts html
+ if (ev.Server.Accepts(ContentType.Html) && config.FailureFiles.TryGetValue(errorCode, out FileCache? ff))
+ {
+ ev.Server.SetNoCache();
+ ev.CloseResponse(errorCode, ContentType.Html, ff.GetReader());
+ return true;
+ }
+ return false;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
+ public string TranslateResourcePath(string requestPath)
+ {
+
+ /*
+ * This function must safely translate a request URL as "unsanitized"
+ * user input to a safe filesystem path for a desired resource.
+ *
+ * A user may supply a custom regex file as a first line of defense
+ * for illegal fs characters.
+ *
+ * It is safe to assume the path is a local path, (not absolute) so
+ * it should not contain an illegal FS scheme
+ */
+
+ requestPath = config.PathFilter?.Replace(requestPath, string.Empty) ?? requestPath;
+
+ using UnsafeMemoryHandle<char> charBuffer = MemoryUtil.UnsafeAlloc<char>(FILE_PATH_BUILDER_BUFFER_SIZE);
+
+ ForwardOnlyWriter<char> sb = new(charBuffer.Span);
+
+ //Start with the root filename
+ sb.Append(config.RootDir.FullName);
+
+ //Supply a "leading" dir separator character
+ if (requestPath[0] != '/')
+ {
+ sb.Append('/');
+ }
+
+ //Add the path (trimmed for whitespace)
+ sb.Append(requestPath);
+
+ //Attmept to filter traversals
+ sb.Replace("..", string.Empty);
+
+ //if were on windows, convert to windows directory separators
+ if (OperatingSystem.IsWindows())
+ {
+ sb.Replace("/", "\\");
+ }
+ else
+ {
+ sb.Replace("\\", "/");
+ }
+
+ /*
+ * DEFAULT: If no file extension is listed or, is not a / separator, then
+ * add a .html extension
+ */
+ if (!Path.EndsInDirectorySeparator(requestPath) && !Path.HasExtension(requestPath))
+ {
+ sb.AppendSmall(".html");
+ }
+
+ return sb.ToString();
+ }
+
+ public void PreProcessEntityAsync(HttpEntity entity, out FileProcessArgs args)
+ {
+ args = FileProcessArgs.Continue;
+ }
+
+ public void PostProcessFile(HttpEntity entity, ref FileProcessArgs chosenRoutine)
+ {
+ //Do not respond to virtual processors
+ if (chosenRoutine == FileProcessArgs.VirtualSkip)
+ {
+ return;
+ }
+
+ //Get-set the x-content options headers from the client config
+ config.TrySetSpecialHeader(entity.Server, SpecialHeaders.XContentOption);
+
+ //Get the re-written url or
+ ReadOnlySpan<char> ext;
+ switch (chosenRoutine.Routine)
+ {
+ case FpRoutine.Deny:
+ case FpRoutine.Error:
+ case FpRoutine.NotFound:
+ case FpRoutine.Redirect:
+ {
+ ReadOnlySpan<char> filePath = entity.Server.Path.AsSpan();
+
+ //disable cache
+ entity.Server.SetNoCache();
+
+ //If the file is an html file or does not include an extension (inferred html)
+ ext = Path.GetExtension(filePath);
+ }
+ break;
+ case FpRoutine.ServeOther:
+ case FpRoutine.ServeOtherFQ:
+ {
+ ReadOnlySpan<char> filePath = chosenRoutine.Alternate.AsSpan();
+
+ //Use the alternate file path for extension
+ ext = Path.GetExtension(filePath);
+
+ //Set default cache
+ ContentType ct = HttpHelpers.GetContentTypeFromFile(filePath);
+ SetCache(entity, ct);
+ }
+ break;
+ default:
+ {
+ ReadOnlySpan<char> filePath = entity.Server.Path.AsSpan();
+
+ //If the file is an html file or does not include an extension (inferred html)
+ ext = Path.GetExtension(filePath);
+ if (ext.IsEmpty)
+ {
+ //If no extension, use .html extension
+ SetCache(entity, ContentType.Html);
+ }
+ else
+ {
+ //Set default cache
+ ContentType ct = HttpHelpers.GetContentTypeFromFile(filePath);
+ SetCache(entity, ct);
+ }
+ }
+ break;
+ }
+
+ //if the file is an html file, we are setting the csp and xss special headers
+ if (ext.IsEmpty || ext.Equals(".html", StringComparison.OrdinalIgnoreCase))
+ {
+ //Get/set xss protection header
+ config.TrySetSpecialHeader(entity.Server, SpecialHeaders.XssProtection);
+ config.TrySetSpecialHeader(entity.Server, SpecialHeaders.ContentSecPolicy);
+ }
+
+ //Set language of the server's os if the user code did not set it
+ if (!entity.Server.Headers.HeaderSet(HttpResponseHeader.ContentLanguage))
+ {
+ entity.Server.Headers[HttpResponseHeader.ContentLanguage] = CultreInfo;
+ }
+ }
+
+ private void SetCache(HttpEntity entity, ContentType ct)
+ {
+ //If request issued no cache request, set nocache headers
+ if (!entity.Server.NoCache())
+ {
+ //Otherwise set caching based on the file extension type
+ switch (ct)
+ {
+ case ContentType.Css:
+ case ContentType.Jpeg:
+ case ContentType.Javascript:
+ case ContentType.Svg:
+ case ContentType.Img:
+ case ContentType.Png:
+ case ContentType.Apng:
+ case ContentType.Avi:
+ case ContentType.Avif:
+ case ContentType.Gif:
+ entity.Server.Headers[HttpResponseHeader.CacheControl] = DefaultCacheString;
+ return;
+ case ContentType.NonSupported:
+ return;
+ default:
+ break;
+ }
+ }
+ entity.Server.SetNoCache();
+ }
+ }
+} \ No newline at end of file
diff --git a/apps/VNLib.WebServer/src/sample.config.json b/apps/VNLib.WebServer/src/sample.config.json
new file mode 100644
index 0000000..a7d268b
--- /dev/null
+++ b/apps/VNLib.WebServer/src/sample.config.json
@@ -0,0 +1,167 @@
+{
+
+ //Host application config, config is loaded as a read-only DOM that is available
+ //to the host and loaded child plugins, all elements are available to plugins via the 'HostConfig' property
+
+ "tcp": {
+ "keepalive_sec": 0, //How long to wait for a keepalive response before closing the connection (0 to disable tcp keepalive)
+ "keepalive_interval_sec": 0, //How long to wait between keepalive probes
+ "max_recv_size": 655360, //640k absolute maximum recv buffer (defaults to OS socket buffer size)
+ "max_connections": 50000, //Per listener instance
+ "backlog": 1000, //OS socket backlog,
+
+ "tx_buffer": 65536, //OS socket send buffer size
+ "rx_buffer": 65536 //OS socket recv buffer size
+ },
+
+ "http": {
+ "default_version": "HTTP/1.1", //The defaut HTTP version to being requests with (does not support http/2 yet)
+ "multipart_max_buf_size": 20480, //The size of the buffer to use when parsing multipart/form data uploads
+ "multipart_max_size": 80240, //The maxium ammount of data (in bytes) allows for mulitpart/form data file uploads
+ "max_entity_size": 1024000, //Absolute maximum size (in bytes) of the request entity body (exludes headers)
+ "keepalive_ms": 1000000, //Keepalive ms for HTTP1.1 keepalive connections
+ "header_buf_size": 8128, //The buffer size to use when parsing headers (also the maxium request header size allowed)
+ "max_request_header_count": 50, //The maxium number of headers allowed in an HTTP request message
+ "max_connections": 5000, //The maxium number of allowed network connections, before 503s will be issued automatically and connections closed
+ "recv_timeout_ms": 5000, //time (in ms) to wait for a response from an active connection in recv mode, before dropping it
+ "send_timeout_ms": 60000, //Time in ms to wait for the client to accept transport data before terminating the connection
+ "response_header_buf_size": 16384, //The size (in bytes) of the buffer used to store all response header data
+ "max_uploads_per_request": 10, //Max number of multi-part file uploads allowed per request
+
+ "compression": {
+ "enabled": true, //controls compression globally
+ "assembly": "", //A custom assembly path (ex: 'VNLib.Net.Compression.dll')
+ "max_size": 512000, //Maxium size of a response to compress before it's bypassed
+ "min_size": 2048 //Minium size of a response to compress, if smaller compression is bypassed
+ }
+ },
+
+ //Maxium ammount of time a request is allowed to be processed (includes loading or waiting for sessions) before operations will be cancelled and a 503 returned
+ "max_execution_time_ms": 20000,
+
+ //Collection of objects to define hosts+interfaces to build server listeners from
+ "virtual_hosts": [
+ {
+
+ //The directory path for files served by this endpoint
+ "path": "path/to/website/root",
+
+ //The hostname to listen for, "*" as wildcard, and "[system]" as the default hostname for the current machine. Must be unique
+ "hostnames": [ "*", "localhost" ],
+
+ "trace": false, //Enables connection trace logging for this endpoint
+ "force_port_check": false, //If set, requires the port in the host header to match the transport port
+
+ //Enable synthetic benchmarking
+ "benchmark": {
+ "enabled": false,
+ "random": true,
+ "size": 128
+ },
+
+ //The interface to bind to, you may not mix TLS and non-TLS connections on the same interface
+ "interfaces": [
+ {
+ "address": "0.0.0.0",
+ "port": 8080,
+
+ "ssl": true, //Enables TLS for this interface for this host specifically
+ "certificate": "/path/to/cert.pfx|pem", //Cert may be pem or pfx (include private key in pfx, or include private key in a pem file)
+ "private_key": "/path/to/private_key.pem", //A pem encoded private key, REQUIRED if using a PEM certificate, may be encrypted with a password
+ "password": "plain-text-password", //An optional password for the ssl private key
+ "client_cert_required": false, //requires that any client connecting to this host present a valid certificate
+ "use_os_ciphers": false //Use the OS's ciphers instead of the hard-coded ciphers
+ }
+ ],
+
+
+ //Collection of "trusted" servers to allow proxy header support from
+ "downstream_servers": [ "127.0.0.1" ],
+
+ /*
+ Specify a list of ip addresses that are allowed to connect to the server, 403 will be returned if connections are not on this list
+ whitelist works behind a trusted downstream server that supports X-Forwared-For headers
+ */
+ "whitelist": [ "127.0.0.1" ],
+
+ "blacklist": [ "127.0.0.1" ], //Individual IP addresses to blacklist
+
+ //A list of file extensions to deny access to, if a resource is requested and has one of the following extensions, a 404 is returned
+ "deny_extensions": [ ".env", ".htaccess", ".php", ".gitignore" ],
+
+ //The default file extensions to append to a resource that does not have a file extension
+ "default_files": [ "index.html", "index.htm" ],
+
+ //Key-value headers object, some headers are special and are controlled by the vh processor
+ "headers": {
+ "header1": "header-value"
+ },
+
+ "cors": {
+ "enabled": true, //Enables cors protections for this host
+ "deny_cors_connections": false, //If true, all cors connections will be denied
+ "allowed_origins": [ "localhost:8089" ]
+ },
+
+ //A list of error file objects, files are loaded into memory (and watched for changes) and returned when the specified error code occurs
+ "error_files": [
+ {
+ "code": 404,
+ "path": "path/to/404"
+ },
+ {
+ "code": 403,
+ "path": "path/to/403"
+ }
+ ],
+
+ //Default http cache time for files
+ "cache_default_sec": 864000
+ }
+ ],
+
+
+ //Defines the directory where plugin's are to be loaded from
+ "plugins": {
+ "enabled": true, //Enable plugin loading
+ //Hot-reload creates collectable assemblies that allow full re-load support in the host application, should only be used for development purposes!
+ "hot_reload": false,
+ "reload_delay_sec": 2,
+ "paths": [
+ "/path/to/plugins_dir"
+ ]
+ //"assets":"",
+ //"config_dir": ""
+ },
+
+ "sys_log": {
+ //"path": "path/to/syslog/file",
+ //"template": "serilog template for writing to file",
+ //"flush_sec": 5,
+ //"retained_files": 31,
+ //"file_size_limit": 10485760,
+ //"interval": "infinite"
+ },
+
+ "app_log": {
+ //"path": "path/to/applog/file",
+ //"template": "serilog template for writing to file",
+ //"flush_sec": 5,
+ //"retained_files": 31,
+ //"file_size_limit": 10485760,
+ //"interval": "infinite"
+ },
+
+
+ //Global secrets object, used by the host and pluings for a specialized secrets
+ "secrets": {
+
+ }
+
+ //global or local configuration to define custom password hashing requirements instead of defaults
+ /*
+ "passwords": {
+
+ }
+ */
+}