diff options
Diffstat (limited to 'apps/VNLib.WebServer/src/VirtualHosts')
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 |