aboutsummaryrefslogtreecommitdiff
path: root/apps/VNLib.WebServer/src/Bootstrap
diff options
context:
space:
mode:
Diffstat (limited to 'apps/VNLib.WebServer/src/Bootstrap')
-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
3 files changed, 593 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();
+ }
+}