aboutsummaryrefslogtreecommitdiff
path: root/apps/VNLib.WebServer/src/VirtualHosts
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-08-01 21:13:04 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2024-08-01 21:13:04 -0400
commit904560a7b5eafd7580fb0a03e778d1751e72a503 (patch)
tree9ffc07d9f9dd6a9106b8cd695a6caa591aac8e95 /apps/VNLib.WebServer/src/VirtualHosts
parent6af95e61212611908d39235222474d4038e10fcd (diff)
build(app): swallow vnlib.webserver into core & build updates
Diffstat (limited to 'apps/VNLib.WebServer/src/VirtualHosts')
-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
6 files changed, 848 insertions, 0 deletions
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