The server has encountered an error and was not able to process your request.
");
+ text.Write("
Please contact the server administrator");
+
+ if (!string.IsNullOrEmpty(ContactInformation))
+ text.Write(" ({0})", WebUtility.HtmlEncode(ContactInformation));
+
+ text.Write(", informing them of the time this error occurred and the action(s) you performed that resulted in this error.
");
+ text.Write("
The following information may help them in finding out what happened and restoring full functionality.
",
+ WebUtility.HtmlEncode(exception.StackTrace));
+ }
+ });
+
+ internal static async Task Handle(string logSource, IHttpContext context, Exception exception, ExceptionHandlerCallback? handler, HttpExceptionHandlerCallback? httpHandler)
+ {
+ if (handler == null)
+ {
+ ExceptionDispatchInfo.Capture(exception).Throw();
+ return;
+ }
+
+ exception.Log(logSource, $"[{context.Id}] Unhandled exception.");
+
+ try
+ {
+ context.Response.SetEmptyResponse((int)HttpStatusCode.InternalServerError);
+ context.Response.DisableCaching();
+ await handler(context, exception)
+ .ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested)
+ {
+ throw;
+ }
+ catch (HttpListenerException)
+ {
+ throw;
+ }
+ catch (Exception httpException) when (httpException is IHttpException httpException1)
+ {
+ if (httpHandler == null)
+ throw;
+
+ await httpHandler(context, httpException1).ConfigureAwait(false);
+ }
+ catch (Exception exception2)
+ {
+ exception2.Log(logSource, $"[{context.Id}] Unhandled exception while handling exception.");
+ }
+ }
+ }
+}
diff --git a/Vendor/EmbedIO-3.5.2/ExceptionHandlerCallback.cs b/Vendor/EmbedIO-3.5.2/ExceptionHandlerCallback.cs
new file mode 100644
index 0000000..406dda2
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/ExceptionHandlerCallback.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace EmbedIO
+{
+ ///
+ /// A callback used to provide information about an unhandled exception occurred while processing a request.
+ ///
+ /// An interface representing the context of the request.
+ /// The unhandled exception.
+ /// A representing the ongoing operation.
+ ///
+ /// When this delegate is called, the response's status code has already been set to
+ /// .
+ /// Any exception thrown by a handler (even a HTTP exception) will go unhandled: the web server
+ /// will not crash, but processing of the request will be aborted, and the response will be flushed as-is.
+ /// In other words, it is not a good ides to throw HttpException.NotFound() (or similar)
+ /// from a handler.
+ ///
+ public delegate Task ExceptionHandlerCallback(IHttpContext context, Exception exception);
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/DirectoryLister.cs b/Vendor/EmbedIO-3.5.2/Files/DirectoryLister.cs
new file mode 100644
index 0000000..d99ee89
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/DirectoryLister.cs
@@ -0,0 +1,20 @@
+using EmbedIO.Files.Internal;
+
+namespace EmbedIO.Files
+{
+ ///
+ /// Provides standard directory listers for .
+ ///
+ ///
+ public static class DirectoryLister
+ {
+ ///
+ /// Gets an interface
+ /// that produces a HTML listing of a directory.
+ /// The output of the returned directory lister
+ /// is the same as a directory listing obtained
+ /// by EmbedIO version 2.
+ ///
+ public static IDirectoryLister Html => HtmlDirectoryLister.Instance;
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/FileCache.Section.cs b/Vendor/EmbedIO-3.5.2/Files/FileCache.Section.cs
new file mode 100644
index 0000000..e133f14
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/FileCache.Section.cs
@@ -0,0 +1,185 @@
+using System;
+using System.Collections.Generic;
+using EmbedIO.Files.Internal;
+
+namespace EmbedIO.Files
+{
+ public sealed partial class FileCache
+ {
+ internal class Section
+ {
+ private readonly object _syncRoot = new object();
+ private readonly Dictionary _items = new Dictionary(StringComparer.Ordinal);
+ private long _totalSize;
+ private string? _oldestKey;
+ private string? _newestKey;
+
+ public void Clear()
+ {
+ lock (_syncRoot)
+ {
+ ClearCore();
+ }
+ }
+
+ public void Add(string path, FileCacheItem item)
+ {
+ lock (_syncRoot)
+ {
+ AddItemCore(path, item);
+ }
+ }
+
+ public void Remove(string path)
+ {
+ lock (_syncRoot)
+ {
+ RemoveItemCore(path);
+ }
+ }
+
+ public bool TryGet(string path, out FileCacheItem item)
+ {
+ lock (_syncRoot)
+ {
+ if (!_items.TryGetValue(path, out item))
+ return false;
+
+ RefreshItemCore(path, item);
+ return true;
+ }
+ }
+
+ internal long GetLeastRecentUseTime()
+ {
+ lock (_syncRoot)
+ {
+ return _oldestKey == null ? long.MaxValue : _items[_oldestKey].LastUsedAt;
+ }
+ }
+
+ // Removes least recently used item.
+ // Returns size of removed item.
+ internal long RemoveLeastRecentItem()
+ {
+ lock (_syncRoot)
+ {
+ return RemoveLeastRecentItemCore();
+ }
+ }
+
+ internal long GetTotalSize()
+ {
+ lock (_syncRoot)
+ {
+ return _totalSize;
+ }
+ }
+
+ internal void UpdateTotalSize(long delta)
+ {
+ lock (_syncRoot)
+ {
+ _totalSize += delta;
+ }
+ }
+
+ private void ClearCore()
+ {
+ _items.Clear();
+ _totalSize = 0;
+ _oldestKey = null;
+ _newestKey = null;
+ }
+
+ // Adds an item as most recently used.
+ private void AddItemCore(string path, FileCacheItem item)
+ {
+ item.PreviousKey = _newestKey;
+ item.NextKey = null;
+ item.LastUsedAt = TimeBase.ElapsedTicks;
+
+ if (_newestKey != null)
+ _items[_newestKey].NextKey = path;
+
+ _newestKey = path;
+
+ _items[path] = item;
+ _totalSize += item.SizeInCache;
+ }
+
+ // Removes an item.
+ private void RemoveItemCore(string path)
+ {
+ if (!_items.TryGetValue(path, out var item))
+ return;
+
+ if (_oldestKey == path)
+ _oldestKey = item.NextKey;
+
+ if (_newestKey == path)
+ _newestKey = item.PreviousKey;
+
+ if (item.PreviousKey != null)
+ _items[item.PreviousKey].NextKey = item.NextKey;
+
+ if (item.NextKey != null)
+ _items[item.NextKey].PreviousKey = item.PreviousKey;
+
+ item.PreviousKey = null;
+ item.NextKey = null;
+
+ _items.Remove(path);
+ _totalSize -= item.SizeInCache;
+ }
+
+ // Removes the least recently used item.
+ // returns size of removed item.
+ private long RemoveLeastRecentItemCore()
+ {
+ var path = _oldestKey;
+ if (path == null)
+ return 0;
+
+ var item = _items[path];
+
+ if ((_oldestKey = item.NextKey) != null)
+ _items[_oldestKey].PreviousKey = null;
+
+ if (_newestKey == path)
+ _newestKey = null;
+
+ item.PreviousKey = null;
+ item.NextKey = null;
+
+ _items.Remove(path);
+ _totalSize -= item.SizeInCache;
+ return item.SizeInCache;
+ }
+
+ // Moves an item to most recently used.
+ private void RefreshItemCore(string path, FileCacheItem item)
+ {
+ item.LastUsedAt = TimeBase.ElapsedTicks;
+
+ if (_newestKey == path)
+ return;
+
+ if (_oldestKey == path)
+ _oldestKey = item.NextKey;
+
+ if (item.PreviousKey != null)
+ _items[item.PreviousKey].NextKey = item.NextKey;
+
+ if (item.NextKey != null)
+ _items[item.NextKey].PreviousKey = item.PreviousKey;
+
+ item.PreviousKey = _newestKey;
+ item.NextKey = null;
+
+ _items[_newestKey!].NextKey = path;
+ _newestKey = path;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/FileCache.cs b/Vendor/EmbedIO-3.5.2/Files/FileCache.cs
new file mode 100644
index 0000000..accf545
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/FileCache.cs
@@ -0,0 +1,178 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using EmbedIO.Internal;
+using Swan.Threading;
+using Swan.Logging;
+
+namespace EmbedIO.Files
+{
+#pragma warning disable CA1001 // Type owns disposable field '_cleaner' but is not disposable - _cleaner has its own dispose semantics.
+ ///
+ /// A cache where one or more instances of can store hashes and file contents.
+ ///
+ public sealed partial class FileCache
+#pragma warning restore CA1001
+ {
+ ///
+ /// The default value for the property.
+ ///
+ public const int DefaultMaxSizeKb = 10240;
+
+ ///
+ /// The default value for the property.
+ ///
+ public const int DefaultMaxFileSizeKb = 200;
+
+ private static readonly Stopwatch TimeBase = Stopwatch.StartNew();
+
+ private static readonly object DefaultSyncRoot = new object();
+ private static FileCache? _defaultInstance;
+
+ private readonly ConcurrentDictionary _sections = new ConcurrentDictionary(StringComparer.Ordinal);
+ private int _sectionCount; // Because ConcurrentDictionary<,>.Count is locking.
+ private int _maxSizeKb = DefaultMaxSizeKb;
+ private int _maxFileSizeKb = DefaultMaxFileSizeKb;
+ private PeriodicTask? _cleaner;
+
+ ///
+ /// Gets the default instance used by .
+ ///
+ public static FileCache Default
+ {
+ get
+ {
+ if (_defaultInstance != null)
+ return _defaultInstance;
+
+ lock (DefaultSyncRoot)
+ {
+ if (_defaultInstance == null)
+ _defaultInstance = new FileCache();
+ }
+
+ return _defaultInstance;
+ }
+ }
+
+ ///
+ /// Gets or sets the maximum total size of cached data in kilobytes (1 kilobyte = 1024 bytes).
+ /// The default value for this property is stored in the constant field.
+ /// Setting this property to a value less lower han 1 has the same effect as setting it to 1.
+ ///
+ public int MaxSizeKb
+ {
+ get => _maxSizeKb;
+ set => _maxSizeKb = Math.Max(value, 1);
+ }
+
+ ///
+ /// Gets or sets the maximum size of a single cached file in kilobytes (1 kilobyte = 1024 bytes).
+ /// A single file's contents may be present in a cache more than once, if the file
+ /// is requested with different Accept-Encoding request headers. This property acts as a threshold
+ /// for the uncompressed size of a file.
+ /// The default value for this property is stored in the constant field.
+ /// Setting this property to a value lower than 0 has the same effect as setting it to 0, in fact
+ /// completely disabling the caching of file contents for this cache.
+ /// This property cannot be set to a value higher than 2097151; in other words, it is not possible
+ /// to cache files bigger than two Gigabytes (1 Gigabyte = 1048576 kilobytes) minus 1 kilobyte.
+ ///
+ public int MaxFileSizeKb
+ {
+ get => _maxFileSizeKb;
+ set => _maxFileSizeKb = Math.Min(Math.Max(value, 0), 2097151);
+ }
+
+ // Cast as IDictionary because we WANT an exception to be thrown if the name exists.
+ // It would mean that something is very, very wrong.
+ internal Section AddSection(string name)
+ {
+ var section = new Section();
+ (_sections as IDictionary).Add(name, section);
+
+ if (Interlocked.Increment(ref _sectionCount) == 1)
+ _cleaner = new PeriodicTask(TimeSpan.FromMinutes(1), CheckMaxSize);
+
+ return section;
+ }
+
+ internal void RemoveSection(string name)
+ {
+ _sections.TryRemove(name, out _);
+
+ if (Interlocked.Decrement(ref _sectionCount) == 0)
+ {
+ _cleaner?.Dispose();
+ _cleaner = null;
+ }
+ }
+
+ private async Task CheckMaxSize(CancellationToken cancellationToken)
+ {
+ var timeKeeper = new TimeKeeper();
+ var maxSizeKb = _maxSizeKb;
+ var initialSizeKb = ComputeTotalSize() / 1024L;
+
+ if (initialSizeKb <= maxSizeKb)
+ {
+ $"Total size = {initialSizeKb}/{_maxSizeKb}kb, not purging.".Debug(nameof(FileCache));
+ return;
+ }
+
+ $"Total size = {initialSizeKb}/{_maxSizeKb}kb, purging...".Debug(nameof(FileCache));
+
+ var removedCount = 0;
+ var removedSize = 0L;
+ var totalSizeKb = initialSizeKb;
+ var threshold = 973L * maxSizeKb / 1024L; // About 95% of maximum allowed size
+ while (totalSizeKb > threshold)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ return;
+
+ var section = GetSectionWithLeastRecentItem();
+ if (section == null)
+ return;
+
+ removedSize += section.RemoveLeastRecentItem();
+ removedCount++;
+
+ await Task.Yield();
+
+ totalSizeKb = ComputeTotalSize() / 1024L;
+ }
+
+ $"Purge completed in {timeKeeper.ElapsedTime}ms: removed {removedCount} items ({removedSize / 1024L}kb). Total size is now {totalSizeKb}kb."
+ .Debug(nameof(FileCache));
+ }
+
+ // Enumerate key / value pairs because the Keys and Values property
+ // of ConcurrentDictionary<,> have snapshot semantics,
+ // while GetEnumerator enumerates without locking.
+ private long ComputeTotalSize()
+ => _sections.Sum(pair => pair.Value.GetTotalSize());
+
+ private Section? GetSectionWithLeastRecentItem()
+ {
+ Section? result = null;
+ var earliestTime = long.MaxValue;
+ foreach (var pair in _sections)
+ {
+ var section = pair.Value;
+ var time = section.GetLeastRecentUseTime();
+
+ if (time < earliestTime)
+ {
+ result = section;
+ earliestTime = time;
+ }
+ }
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/FileModule.cs b/Vendor/EmbedIO-3.5.2/Files/FileModule.cs
new file mode 100644
index 0000000..5dccd56
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/FileModule.cs
@@ -0,0 +1,635 @@
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using EmbedIO.Files.Internal;
+using EmbedIO.Internal;
+using EmbedIO.Utilities;
+
+namespace EmbedIO.Files
+{
+ ///
+ /// A module serving files and directory listings from an .
+ ///
+ ///
+ public class FileModule : WebModuleBase, IDisposable, IMimeTypeCustomizer
+ {
+ ///
+ /// Default value for .
+ ///
+ public const string DefaultDocumentName = "index.html";
+
+ private readonly string _cacheSectionName = UniqueIdGenerator.GetNext();
+ private readonly MimeTypeCustomizer _mimeTypeCustomizer = new MimeTypeCustomizer();
+ private readonly ConcurrentDictionary? _mappingCache;
+
+ private FileCache _cache = FileCache.Default;
+ private bool _contentCaching = true;
+ private string? _defaultDocument = DefaultDocumentName;
+ private string? _defaultExtension;
+ private IDirectoryLister? _directoryLister;
+ private FileRequestHandlerCallback _onMappingFailed = FileRequestHandler.ThrowNotFound;
+ private FileRequestHandlerCallback _onDirectoryNotListable = FileRequestHandler.ThrowUnauthorized;
+ private FileRequestHandlerCallback _onMethodNotAllowed = FileRequestHandler.ThrowMethodNotAllowed;
+
+ private FileCache.Section? _cacheSection;
+
+ ///
+ /// Initializes a new instance of the class,
+ /// using the specified cache.
+ ///
+ /// The base route.
+ /// An interface that provides access
+ /// to actual files and directories.
+ /// is .
+ public FileModule(string baseRoute, IFileProvider provider)
+ : base(baseRoute)
+ {
+ Provider = Validate.NotNull(nameof(provider), provider);
+ _mappingCache = Provider.IsImmutable
+ ? new ConcurrentDictionary()
+ : null;
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~FileModule()
+ {
+ Dispose(false);
+ }
+
+ ///
+ public override bool IsFinalHandler => true;
+
+ ///
+ /// Gets the interface that provides access
+ /// to actual files and directories served by this module.
+ ///
+ public IFileProvider Provider { get; }
+
+ ///
+ /// Gets or sets the used by this module to store hashes and,
+ /// optionally, file contents and rendered directory listings.
+ ///
+ /// The module's configuration is locked.
+ /// This property is being set to .
+ public FileCache Cache
+ {
+ get => _cache;
+ set
+ {
+ EnsureConfigurationNotLocked();
+ _cache = Validate.NotNull(nameof(value), value);
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this module caches the contents of files
+ /// and directory listings.
+ /// Note that the actual representations of files are stored in ;
+ /// thus, for example, if a file is always requested with an Accept-Encoding of gzip,
+ /// only the gzipped contents of the file will be cached.
+ ///
+ /// The module's configuration is locked.
+ public bool ContentCaching
+ {
+ get => _contentCaching;
+ set
+ {
+ EnsureConfigurationNotLocked();
+ _contentCaching = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the name of the default document served, if it exists, instead of a directory listing
+ /// when the path of a requested URL maps to a directory.
+ /// The default value for this property is the constant.
+ ///
+ /// The module's configuration is locked.
+ public string? DefaultDocument
+ {
+ get => _defaultDocument;
+ set
+ {
+ EnsureConfigurationNotLocked();
+ _defaultDocument = string.IsNullOrEmpty(value) ? null : value;
+ }
+ }
+
+ ///
+ /// Gets or sets the default extension appended to requested URL paths that do not map
+ /// to any file or directory. Defaults to .
+ ///
+ /// The module's configuration is locked.
+ /// This property is being set to a non-,
+ /// non-empty string that does not start with a period (.).
+ public string? DefaultExtension
+ {
+ get => _defaultExtension;
+ set
+ {
+ EnsureConfigurationNotLocked();
+
+ if (string.IsNullOrEmpty(value))
+ {
+ _defaultExtension = null;
+ }
+ else if (value![0] != '.')
+ {
+ throw new ArgumentException("Default extension does not start with a period.", nameof(value));
+ }
+ else
+ {
+ _defaultExtension = value;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the interface used to generate
+ /// directory listing in this module.
+ /// A value of (the default) disables the generation
+ /// of directory listings.
+ ///
+ /// The module's configuration is locked.
+ public IDirectoryLister? DirectoryLister
+ {
+ get => _directoryLister;
+ set
+ {
+ EnsureConfigurationNotLocked();
+ _directoryLister = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a that is called whenever
+ /// the requested URL path could not be mapped to any file or directory.
+ /// The default is .
+ ///
+ /// The module's configuration is locked.
+ /// This property is being set to .
+ ///
+ public FileRequestHandlerCallback OnMappingFailed
+ {
+ get => _onMappingFailed;
+ set
+ {
+ EnsureConfigurationNotLocked();
+ _onMappingFailed = Validate.NotNull(nameof(value), value);
+ }
+ }
+
+ ///
+ /// Gets or sets a that is called whenever
+ /// the requested URL path has been mapped to a directory, but directory listing has been
+ /// disabled by setting to .
+ /// The default is .
+ ///
+ /// The module's configuration is locked.
+ /// This property is being set to .
+ ///
+ public FileRequestHandlerCallback OnDirectoryNotListable
+ {
+ get => _onDirectoryNotListable;
+ set
+ {
+ EnsureConfigurationNotLocked();
+ _onDirectoryNotListable = Validate.NotNull(nameof(value), value);
+ }
+ }
+
+ ///
+ /// Gets or sets a that is called whenever
+ /// the requested URL path has been mapped to a file or directory, but the request's
+ /// HTTP method is neither GET nor HEAD.
+ /// The default is .
+ ///
+ /// The module's configuration is locked.
+ /// This property is being set to .
+ ///
+ public FileRequestHandlerCallback OnMethodNotAllowed
+ {
+ get => _onMethodNotAllowed;
+ set
+ {
+ EnsureConfigurationNotLocked();
+ _onMethodNotAllowed = Validate.NotNull(nameof(value), value);
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ string IMimeTypeProvider.GetMimeType(string extension)
+ => _mimeTypeCustomizer.GetMimeType(extension);
+
+ bool IMimeTypeProvider.TryDetermineCompression(string mimeType, out bool preferCompression)
+ => _mimeTypeCustomizer.TryDetermineCompression(mimeType, out preferCompression);
+
+ ///
+ public void AddCustomMimeType(string extension, string mimeType)
+ => _mimeTypeCustomizer.AddCustomMimeType(extension, mimeType);
+
+ ///
+ public void PreferCompression(string mimeType, bool preferCompression)
+ => _mimeTypeCustomizer.PreferCompression(mimeType, preferCompression);
+
+ ///
+ /// Clears the part of used by this module.
+ ///
+ public void ClearCache()
+ {
+ _mappingCache?.Clear();
+ _cacheSection?.Clear();
+ }
+
+ ///
+ /// Releases unmanaged and - optionally - managed resources.
+ ///
+ /// to release both managed and unmanaged resources;
+ /// to release only unmanaged resources.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposing)
+ return;
+
+ if (_cacheSection != null)
+ Provider.ResourceChanged -= _cacheSection.Remove;
+
+ if (Provider is IDisposable disposableProvider)
+ disposableProvider.Dispose();
+
+ if (_cacheSection != null)
+ Cache.RemoveSection(_cacheSectionName);
+ }
+
+ ///
+ protected override void OnBeforeLockConfiguration()
+ {
+ base.OnBeforeLockConfiguration();
+
+ _mimeTypeCustomizer.Lock();
+ }
+
+ ///
+ protected override void OnStart(CancellationToken cancellationToken)
+ {
+ base.OnStart(cancellationToken);
+
+ _cacheSection = Cache.AddSection(_cacheSectionName);
+ Provider.ResourceChanged += _cacheSection.Remove;
+ Provider.Start(cancellationToken);
+ }
+
+ ///
+ protected override async Task OnRequestAsync(IHttpContext context)
+ {
+ MappedResourceInfo? info;
+
+ var path = context.RequestedPath;
+
+ // Map the URL path to a mapped resource.
+ // DefaultDocument and DefaultExtension are handled here.
+ // Use the mapping cache if it exists.
+ if (_mappingCache == null)
+ {
+ info = MapUrlPath(path, context);
+ }
+ else if (!_mappingCache.TryGetValue(path, out info))
+ {
+ info = MapUrlPath(path, context);
+ if (info != null)
+ _ = _mappingCache.AddOrUpdate(path, info, (_, __) => info);
+ }
+
+ if (info == null)
+ {
+ // If mapping failed, send a "404 Not Found" response, or whatever OnMappingFailed chooses to do.
+ // For example, it may return a default resource (think a folder of images and an imageNotFound.jpg),
+ // or redirect the request.
+ await OnMappingFailed(context, null).ConfigureAwait(false);
+ }
+ else if (!IsHttpMethodAllowed(context.Request, out var sendResponseBody))
+ {
+ // If there is a mapped resource, check that the HTTP method is either GET or HEAD.
+ // Otherwise, send a "405 Method Not Allowed" response, or whatever OnMethodNotAllowed chooses to do.
+ await OnMethodNotAllowed(context, info).ConfigureAwait(false);
+ }
+ else if (info.IsDirectory && DirectoryLister == null)
+ {
+ // If a directory listing was requested, but there is no DirectoryLister,
+ // send a "403 Unauthorized" response, or whatever OnDirectoryNotListable chooses to do.
+ // For example, one could prefer to send "404 Not Found" instead.
+ await OnDirectoryNotListable(context, info).ConfigureAwait(false);
+ }
+ else
+ {
+ await HandleResource(context, info, sendResponseBody).ConfigureAwait(false);
+ }
+ }
+
+ // Tells whether a request's HTTP method is suitable for processing by FileModule
+ // and, if so, whether a response body must be sent.
+ private static bool IsHttpMethodAllowed(IHttpRequest request, out bool sendResponseBody)
+ {
+ switch (request.HttpVerb)
+ {
+ case HttpVerbs.Head:
+ sendResponseBody = false;
+ return true;
+ case HttpVerbs.Get:
+ sendResponseBody = true;
+ return true;
+ default:
+ sendResponseBody = default;
+ return false;
+ }
+ }
+
+ // Prepares response headers for a "200 OK" or "304 Not Modified" response.
+ // RFC7232, Section 4.1
+ private static void PreparePositiveResponse(IHttpResponse response, MappedResourceInfo info, string contentType, string entityTag, Action setCompression)
+ {
+ setCompression(response);
+ response.ContentType = contentType;
+ response.Headers.Set(HttpHeaderNames.ETag, entityTag);
+ response.Headers.Set(HttpHeaderNames.LastModified, HttpDate.Format(info.LastModifiedUtc));
+ response.Headers.Set(HttpHeaderNames.CacheControl, "max-age=0, must-revalidate");
+ response.Headers.Set(HttpHeaderNames.AcceptRanges, "bytes");
+ }
+
+ // Attempts to map a module-relative URL path to a mapped resource,
+ // handling DefaultDocument and DefaultExtension.
+ // Returns null if not found.
+ // Directories mus be returned regardless of directory listing being enabled.
+ private MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider)
+ {
+ var result = Provider.MapUrlPath(urlPath, mimeTypeProvider);
+
+ // If urlPath maps to a file, no further searching is needed.
+ if (result?.IsFile ?? false)
+ return result;
+
+ // Look for a default document.
+ // Don't append an additional slash if the URL path is "/".
+ // The default document, if found, must be a file, not a directory.
+ if (DefaultDocument != null)
+ {
+ var defaultDocumentPath = urlPath + (urlPath.Length > 1 ? "/" : string.Empty) + DefaultDocument;
+ var defaultDocumentResult = Provider.MapUrlPath(defaultDocumentPath, mimeTypeProvider);
+ if (defaultDocumentResult?.IsFile ?? false)
+ return defaultDocumentResult;
+ }
+
+ // Try to apply default extension (but not if the URL path is "/",
+ // i.e. the only normalized, non-base URL path that ends in a slash).
+ // When the default extension is applied, the result must be a file.
+ if (DefaultExtension != null && urlPath.Length > 1)
+ {
+ var defaultExtensionResult = Provider.MapUrlPath(urlPath + DefaultExtension, mimeTypeProvider);
+ if (defaultExtensionResult?.IsFile ?? false)
+ return defaultExtensionResult;
+ }
+
+ return result;
+ }
+
+ private async Task HandleResource(IHttpContext context, MappedResourceInfo info, bool sendResponseBody)
+ {
+ // Try to extract resource information from cache.
+ var cachingThreshold = 1024L * Cache.MaxFileSizeKb;
+ if (!_cacheSection!.TryGet(info.Path, out var cacheItem))
+ {
+ // Resource information not yet cached
+ cacheItem = new FileCacheItem(_cacheSection, info.LastModifiedUtc, info.Length);
+ _cacheSection.Add(info.Path, cacheItem);
+ }
+ else if (!Provider.IsImmutable)
+ {
+ // Check whether the resource has changed.
+ // If so, discard the cache item and create a new one.
+ if (cacheItem.LastModifiedUtc != info.LastModifiedUtc || cacheItem.Length != info.Length)
+ {
+ _cacheSection.Remove(info.Path);
+ cacheItem = new FileCacheItem(_cacheSection, info.LastModifiedUtc, info.Length);
+ _cacheSection.Add(info.Path, cacheItem);
+ }
+ }
+
+ /*
+ * Now we have a cacheItem for the resource.
+ * It may have been just created, or it may or may not have a cached content,
+ * depending upon the value of the ContentCaching property,
+ * the size of the resource, and the value of the
+ * MaxFileSizeKb of our Cache.
+ */
+
+ // If the content type is not a valid MIME type, assume the default.
+ var contentType = info.ContentType ?? DirectoryLister?.ContentType ?? MimeType.Default;
+ var mimeType = MimeType.StripParameters(contentType);
+ if (!MimeType.IsMimeType(mimeType, false))
+ contentType = mimeType = MimeType.Default;
+
+ // Next we're going to apply proactive negotiation
+ // to determine whether we agree with the client upon the compression
+ // (or lack of it) to use for the resource.
+ //
+ // The combination of partial responses and entity compression
+ // is not really standardized and could lead to a world of pain.
+ // Thus, if there is a Range header in the request, try to negotiate for no compression.
+ // Later, if there is compression anyway, we will ignore the Range header.
+ if (!context.TryDetermineCompression(mimeType, out var preferCompression))
+ preferCompression = true;
+ preferCompression &= context.Request.Headers.Get(HttpHeaderNames.Range) == null;
+ if (!context.Request.TryNegotiateContentEncoding(preferCompression, out var compressionMethod, out var setCompressionInResponse))
+ {
+ // If negotiation failed, the returned callback will do the right thing.
+ setCompressionInResponse(context.Response);
+ return;
+ }
+
+ var entityTag = info.GetEntityTag(compressionMethod);
+
+ // Send a "304 Not Modified" response if applicable.
+ //
+ // RFC7232, Section 3.3: "A recipient MUST ignore If-Modified-Since
+ // if the request contains an If-None-Match header field."
+ if (context.Request.CheckIfNoneMatch(entityTag, out var ifNoneMatchExists)
+ || (!ifNoneMatchExists && context.Request.CheckIfModifiedSince(info.LastModifiedUtc, out _)))
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.NotModified;
+ PreparePositiveResponse(context.Response, info, contentType, entityTag, setCompressionInResponse);
+ return;
+ }
+
+ /*
+ * At this point we know the response is "200 OK",
+ * unless the request is a range request.
+ *
+ * RFC7233, Section 3.1: "The Range header field is evaluated after evaluating the precondition
+ * header fields defined in RFC7232, and only if the result in absence
+ * of the Range header field would be a 200 (OK) response. In other
+ * words, Range is ignored when a conditional GET would result in a 304
+ * (Not Modified) response."
+ */
+
+ // Before evaluating ranges, we must know the content length.
+ // This is easy for files, as it is stored in info.Length.
+ // Directories always have info.Length == 0; therefore,
+ // unless the directory listing is cached, we must generate it now
+ // (and cache it while we're there, if applicable).
+ var content = cacheItem.GetContent(compressionMethod);
+ if (info.IsDirectory && content == null)
+ {
+ long uncompressedLength;
+ (content, uncompressedLength) = await GenerateDirectoryListingAsync(context, info, compressionMethod)
+ .ConfigureAwait(false);
+ if (ContentCaching && uncompressedLength <= cachingThreshold)
+ _ = cacheItem.SetContent(compressionMethod, content);
+ }
+
+ var contentLength = content?.Length ?? info.Length;
+
+ // Ignore range request is compression is enabled
+ // (or should I say forced, since negotiation has tried not to use it).
+ var partialStart = 0L;
+ var partialUpperBound = contentLength - 1;
+ var isPartial = compressionMethod == CompressionMethod.None
+ && context.Request.IsRangeRequest(contentLength, entityTag, info.LastModifiedUtc, out partialStart, out partialUpperBound);
+ var responseContentLength = contentLength;
+
+ if (isPartial)
+ {
+ // Prepare a "206 Partial Content" response.
+ responseContentLength = partialUpperBound - partialStart + 1;
+ context.Response.StatusCode = (int)HttpStatusCode.PartialContent;
+ PreparePositiveResponse(context.Response, info, contentType, entityTag, setCompressionInResponse);
+ context.Response.Headers.Set(HttpHeaderNames.ContentRange, $"bytes {partialStart}-{partialUpperBound}/{contentLength}");
+ }
+ else
+ {
+ // Prepare a "200 OK" response.
+ PreparePositiveResponse(context.Response, info, contentType, entityTag, setCompressionInResponse);
+ }
+
+ // If it's a HEAD request, we're done.
+ if (!sendResponseBody)
+ return;
+
+ // If content must be sent AND cached, first read it and store it.
+ // If the requested resource is a directory, we have already listed it by now,
+ // so it must be a file for content to be null.
+ if (content == null && ContentCaching && contentLength <= cachingThreshold)
+ {
+ using (var memoryStream = new MemoryStream())
+ {
+ using (var compressor = new CompressionStream(memoryStream, compressionMethod))
+ {
+ using var source = Provider.OpenFile(info.Path);
+ await source.CopyToAsync(compressor, WebServer.StreamCopyBufferSize, context.CancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ content = memoryStream.ToArray();
+ responseContentLength = content.Length;
+ }
+
+ _ = cacheItem.SetContent(compressionMethod, content);
+ }
+
+ // Transfer cached content if present.
+ if (content != null)
+ {
+ context.Response.ContentLength64 = responseContentLength;
+ var offset = isPartial ? (int) partialStart : 0;
+ await context.Response.OutputStream.WriteAsync(content, offset, (int)responseContentLength, context.CancellationToken)
+ .ConfigureAwait(false);
+
+ return;
+ }
+
+ // Read and transfer content without caching.
+ using (var source = Provider.OpenFile(info.Path))
+ {
+ context.Response.SendChunked = true;
+
+ if (isPartial)
+ {
+ var buffer = new byte[WebServer.StreamCopyBufferSize];
+ if (source.CanSeek)
+ {
+ source.Position = partialStart;
+ }
+ else
+ {
+ var skipLength = (int)partialStart;
+ while (skipLength > 0)
+ {
+ var read = await source.ReadAsync(buffer, 0, Math.Min(skipLength, buffer.Length), context.CancellationToken)
+ .ConfigureAwait(false);
+
+ skipLength -= read;
+ }
+ }
+
+ var transferSize = responseContentLength;
+ while (transferSize >= WebServer.StreamCopyBufferSize)
+ {
+ var read = await source.ReadAsync(buffer, 0, WebServer.StreamCopyBufferSize, context.CancellationToken)
+ .ConfigureAwait(false);
+
+ await context.Response.OutputStream.WriteAsync(buffer, 0, read, context.CancellationToken)
+ .ConfigureAwait(false);
+
+ transferSize -= read;
+ }
+
+ if (transferSize > 0)
+ {
+ var read = await source.ReadAsync(buffer, 0, (int)transferSize, context.CancellationToken)
+ .ConfigureAwait(false);
+
+ await context.Response.OutputStream.WriteAsync(buffer, 0, read, context.CancellationToken)
+ .ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ using var compressor = new CompressionStream(context.Response.OutputStream, compressionMethod);
+ await source.CopyToAsync(compressor, WebServer.StreamCopyBufferSize, context.CancellationToken)
+ .ConfigureAwait(false);
+ }
+ }
+ }
+
+ // Uses DirectoryLister to generate a directory listing asynchronously.
+ // Returns a tuple of the generated content and its *uncompressed* length
+ // (useful to decide whether it can be cached).
+ private async Task<(byte[], long)> GenerateDirectoryListingAsync(
+ IHttpContext context,
+ MappedResourceInfo info,
+ CompressionMethod compressionMethod)
+ {
+ using var memoryStream = new MemoryStream();
+ using var stream = new CompressionStream(memoryStream, compressionMethod);
+
+ await DirectoryLister!.ListDirectoryAsync(
+ info,
+ context.Request.Url.AbsolutePath,
+ Provider.GetDirectoryEntries(info.Path, context),
+ stream,
+ context.CancellationToken).ConfigureAwait(false);
+
+ return (memoryStream.ToArray(), stream.UncompressedLength);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/FileModuleExtensions.cs b/Vendor/EmbedIO-3.5.2/Files/FileModuleExtensions.cs
new file mode 100644
index 0000000..b315747
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/FileModuleExtensions.cs
@@ -0,0 +1,282 @@
+using System;
+
+namespace EmbedIO.Files
+{
+ ///
+ /// Provides extension methods for and derived classes.
+ ///
+ public static class FileModuleExtensions
+ {
+ ///
+ /// Sets the used by a module to store hashes and,
+ /// optionally, file contents and rendered directory listings.
+ ///
+ /// The type of the module on which this method is called.
+ /// The module on which this method is called.
+ /// An instance of .
+ /// with its Cache property
+ /// set to .
+ /// is .
+ /// The configuration of is locked.
+ /// is .
+ ///
+ public static TModule WithCache(this TModule @this, FileCache value)
+ where TModule : FileModule
+ {
+ @this.Cache = value;
+ return @this;
+ }
+
+ ///
+ /// Sets a value indicating whether a module caches the contents of files
+ /// and directory listings.
+ ///
+ /// The type of the module on which this method is called.
+ /// The module on which this method is called.
+ /// to enable caching of contents;
+ /// to disable it.
+ /// with its ContentCaching property
+ /// set to .
+ /// is .
+ /// The configuration of is locked.
+ ///
+ public static TModule WithContentCaching(this TModule @this, bool value)
+ where TModule : FileModule
+ {
+ @this.ContentCaching = value;
+ return @this;
+ }
+
+ ///
+ /// Enables caching of file contents and directory listings on a module.
+ ///
+ /// The type of the module on which this method is called.
+ /// The module on which this method is called.
+ /// with its ContentCaching property
+ /// set to .
+ /// is .
+ /// The configuration of is locked.
+ ///
+ public static TModule WithContentCaching(this TModule @this)
+ where TModule : FileModule
+ {
+ @this.ContentCaching = true;
+ return @this;
+ }
+
+ ///
+ /// Enables caching of file contents and directory listings on a module.
+ ///
+ /// The type of the module on which this method is called.
+ /// The module on which this method is called.
+ /// sets the maximum size of a single cached file in kilobytes
+ /// sets the maximum total size of cached data in kilobytes
+ /// with its ContentCaching property
+ /// set to .
+ /// is .
+ /// The configuration of is locked.
+ ///
+ public static TModule WithContentCaching(this TModule @this, int maxFileSizeKb, int maxSizeKb)
+ where TModule : FileModule
+ {
+ @this.ContentCaching = true;
+ @this.Cache.MaxFileSizeKb = maxFileSizeKb;
+ @this.Cache.MaxSizeKb = maxSizeKb;
+ return @this;
+ }
+
+ ///
+ /// Disables caching of file contents and directory listings on a module.
+ ///
+ /// The type of the module on which this method is called.
+ /// The module on which this method is called.
+ /// with its ContentCaching property
+ /// set to .
+ /// is .
+ /// The configuration of is locked.
+ ///
+ public static TModule WithoutContentCaching(this TModule @this)
+ where TModule : FileModule
+ {
+ @this.ContentCaching = false;
+ return @this;
+ }
+
+ ///
+ /// Sets the name of the default document served, if it exists, instead of a directory listing
+ /// when the path of a requested URL maps to a directory.
+ ///
+ /// The type of the module on which this method is called.
+ /// The module on which this method is called.
+ /// The name of the default document.
+ /// with its DefaultDocument property
+ /// set to .
+ /// is .
+ /// The configuration of is locked.
+ ///
+ public static TModule WithDefaultDocument(this TModule @this, string value)
+ where TModule : FileModule
+ {
+ @this.DefaultDocument = value;
+ return @this;
+ }
+
+ ///
+ /// Sets the name of the default document to .
+ ///
+ /// The type of the module on which this method is called.
+ /// The module on which this method is called.
+ /// with its DefaultDocument property
+ /// set to .
+ /// is .
+ /// The configuration of is locked.
+ ///
+ public static TModule WithoutDefaultDocument(this TModule @this)
+ where TModule : FileModule
+ {
+ @this.DefaultDocument = null;
+ return @this;
+ }
+
+ ///
+ /// Sets the default extension appended to requested URL paths that do not map
+ /// to any file or directory.
+ ///
+ /// The type of the module on which this method is called.
+ /// The module on which this method is called.
+ /// The default extension.
+ /// with its DefaultExtension property
+ /// set to .
+ /// is .
+ /// The configuration of is locked.
+ /// is a non-,
+ /// non-empty string that does not start with a period (.).
+ ///
+ public static TModule WithDefaultExtension(this TModule @this, string value)
+ where TModule : FileModule
+ {
+ @this.DefaultExtension = value;
+ return @this;
+ }
+
+ ///
+ /// Sets the default extension to .
+ ///
+ /// The type of the module on which this method is called.
+ /// The module on which this method is called.
+ /// with its DefaultExtension property
+ /// set to .
+ /// is .
+ /// The configuration of is locked.
+ ///
+ public static TModule WithoutDefaultExtension(this TModule @this)
+ where TModule : FileModule
+ {
+ @this.DefaultExtension = null;
+ return @this;
+ }
+
+ ///
+ /// Sets the interface used to generate
+ /// directory listing in a module.
+ ///
+ /// The type of the module on which this method is called.
+ /// The module on which this method is called.
+ /// An interface, or
+ /// to disable the generation of directory listings.
+ /// with its DirectoryLister property
+ /// set to .
+ /// is .
+ /// The configuration of is locked.
+ ///
+ public static TModule WithDirectoryLister(this TModule @this, IDirectoryLister value)
+ where TModule : FileModule
+ {
+ @this.DirectoryLister = value;
+ return @this;
+ }
+
+ ///
+ /// Sets a module's DirectoryLister property
+ /// to , disabling the generation of directory listings.
+ ///
+ /// The type of the module on which this method is called.
+ /// The module on which this method is called.
+ /// with its DirectoryLister property
+ /// set to .
+ /// is .
+ /// The configuration of is locked.
+ ///
+ public static TModule WithoutDirectoryLister(this TModule @this)
+ where TModule : FileModule
+ {
+ @this.DirectoryLister = null;
+ return @this;
+ }
+
+ ///
+ /// Sets a that is called by a module whenever
+ /// the requested URL path could not be mapped to any file or directory.
+ ///
+ /// The type of the module on which this method is called.
+ /// The module on which this method is called.
+ /// The method to call.
+ /// with its OnMappingFailed property
+ /// set to .
+ /// is .
+ /// The configuration of is locked.
+ /// is .
+ ///
+ ///
+ public static TModule HandleMappingFailed(this TModule @this, FileRequestHandlerCallback callback)
+ where TModule : FileModule
+ {
+ @this.OnMappingFailed = callback;
+ return @this;
+ }
+
+ ///
+ /// Sets a that is called by a module whenever
+ /// the requested URL path has been mapped to a directory, but directory listing has been
+ /// disabled.
+ ///
+ /// The type of the module on which this method is called.
+ /// The module on which this method is called.
+ /// The method to call.
+ /// with its OnDirectoryNotListable property
+ /// set to .
+ /// is .
+ /// The configuration of is locked.
+ /// is .
+ ///
+ ///
+ public static TModule HandleDirectoryNotListable(this TModule @this, FileRequestHandlerCallback callback)
+ where TModule : FileModule
+ {
+ @this.OnDirectoryNotListable = callback;
+ return @this;
+ }
+
+ ///
+ /// Sets a that is called by a module whenever
+ /// the requested URL path has been mapped to a file or directory, but the request's
+ /// HTTP method is neither GET nor HEAD.
+ ///
+ /// The type of the module on which this method is called.
+ /// The module on which this method is called.
+ /// The method to call.
+ /// with its OnMethodNotAllowed property
+ /// set to .
+ /// is .
+ /// The configuration of is locked.
+ /// is .
+ ///
+ ///
+ public static TModule HandleMethodNotAllowed(this TModule @this, FileRequestHandlerCallback callback)
+ where TModule : FileModule
+ {
+ @this.OnMethodNotAllowed = callback;
+ return @this;
+ }
+ }
+}
diff --git a/Vendor/EmbedIO-3.5.2/Files/FileRequestHandler.cs b/Vendor/EmbedIO-3.5.2/Files/FileRequestHandler.cs
new file mode 100644
index 0000000..805917c
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/FileRequestHandler.cs
@@ -0,0 +1,53 @@
+using System.Threading.Tasks;
+
+namespace EmbedIO.Files
+{
+ ///
+ /// Provides standard handler callbacks for .
+ ///
+ ///
+ public static class FileRequestHandler
+ {
+#pragma warning disable CA1801 // Unused parameters - Must respect FileRequestHandlerCallback signature.
+ ///
+ /// Unconditionally passes a request down the module chain.
+ ///
+ /// An interface representing the context of the request.
+ /// If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping;
+ /// otherwise, .
+ /// This method never returns; it throws an exception instead.
+ public static Task PassThrough(IHttpContext context, MappedResourceInfo? info)
+ => throw RequestHandler.PassThrough();
+
+ ///
+ /// Unconditionally sends a 403 Unauthorized response.
+ ///
+ /// An interface representing the context of the request.
+ /// If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping;
+ /// otherwise, .
+ /// This method never returns; it throws a instead.
+ public static Task ThrowUnauthorized(IHttpContext context, MappedResourceInfo? info)
+ => throw HttpException.Unauthorized();
+
+ ///
+ /// Unconditionally sends a 404 Not Found response.
+ ///
+ /// An interface representing the context of the request.
+ /// If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping;
+ /// otherwise, .
+ /// This method never returns; it throws a instead.
+ public static Task ThrowNotFound(IHttpContext context, MappedResourceInfo? info)
+ => throw HttpException.NotFound();
+
+ ///
+ /// Unconditionally sends a 405 Method Not Allowed response.
+ ///
+ /// An interface representing the context of the request.
+ /// If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping;
+ /// otherwise, .
+ /// This method never returns; it throws a instead.
+ public static Task ThrowMethodNotAllowed(IHttpContext context, MappedResourceInfo? info)
+ => throw HttpException.MethodNotAllowed();
+#pragma warning restore CA1801
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/FileRequestHandlerCallback.cs b/Vendor/EmbedIO-3.5.2/Files/FileRequestHandlerCallback.cs
new file mode 100644
index 0000000..0340e14
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/FileRequestHandlerCallback.cs
@@ -0,0 +1,13 @@
+using System.Threading.Tasks;
+
+namespace EmbedIO.Files
+{
+ ///
+ /// A callback used to handle a request in .
+ ///
+ /// An interface representing the context of the request.
+ /// If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping;
+ /// otherwise, .
+ /// A representing the ongoing operation.
+ public delegate Task FileRequestHandlerCallback(IHttpContext context, MappedResourceInfo? info);
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/FileSystemProvider.cs b/Vendor/EmbedIO-3.5.2/Files/FileSystemProvider.cs
new file mode 100644
index 0000000..26f8f21
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/FileSystemProvider.cs
@@ -0,0 +1,202 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
+using EmbedIO.Utilities;
+
+namespace EmbedIO.Files
+{
+ ///
+ /// Provides access to the local file system to a .
+ ///
+ ///
+ public class FileSystemProvider : IDisposable, IFileProvider
+ {
+ private readonly FileSystemWatcher? _watcher;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// OSX doesn't support , the parameter will be always .
+ ///
+ /// The file system path.
+ /// if files and directories in
+ /// are not expected to change during a web server's
+ /// lifetime; otherwise.
+ /// is .
+ /// is not a valid local path.
+ ///
+ public FileSystemProvider(string fileSystemPath, bool isImmutable)
+ {
+ FileSystemPath = Validate.LocalPath(nameof(fileSystemPath), fileSystemPath, true);
+ IsImmutable = isImmutable || RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
+
+ try
+ {
+ if (!IsImmutable)
+ _watcher = new FileSystemWatcher(FileSystemPath);
+ }
+ catch (PlatformNotSupportedException)
+ {
+ IsImmutable = true;
+ }
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~FileSystemProvider()
+ {
+ Dispose(false);
+ }
+
+ ///
+ public event Action? ResourceChanged;
+
+ ///
+ /// Gets the file system path from which files are retrieved.
+ ///
+ public string FileSystemPath { get; }
+
+ ///
+ public bool IsImmutable { get; }
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ public void Start(CancellationToken cancellationToken)
+ {
+ if (_watcher != null)
+ {
+ _watcher.Changed += Watcher_ChangedOrDeleted;
+ _watcher.Deleted += Watcher_ChangedOrDeleted;
+ _watcher.Renamed += Watcher_Renamed;
+ _watcher.EnableRaisingEvents = true;
+ }
+ }
+
+ ///
+ public MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider)
+ {
+ urlPath = urlPath.Substring(1); // Drop the initial slash
+ string localPath;
+
+ // Disable CA1031 as there's little we can do if IsPathRooted or GetFullPath fails.
+#pragma warning disable CA1031
+ try
+ {
+ // Unescape the url before continue
+ urlPath = Uri.UnescapeDataString(urlPath);
+
+ // Bail out early if the path is a rooted path,
+ // as Path.Combine would ignore our base path.
+ // See https://docs.microsoft.com/en-us/dotnet/api/system.io.path.combine
+ // (particularly the Remarks section).
+ //
+ // Under Windows, a relative URL path may be a full filesystem path
+ // (e.g. "D:\foo\bar" or "\\192.168.0.1\Shared\MyDocuments\BankAccounts.docx").
+ // Under Unix-like operating systems we have no such problems, as relativeUrlPath
+ // can never start with a slash; however, loading one more class from Swan
+ // just to check the OS type would probably outweigh calling IsPathRooted.
+ if (Path.IsPathRooted(urlPath))
+ return null;
+
+ // Convert the relative URL path to a relative filesystem path
+ // (practically a no-op under Unix-like operating systems)
+ // and combine it with our base local path to obtain a full path.
+ localPath = Path.Combine(FileSystemPath, urlPath.Replace('/', Path.DirectorySeparatorChar));
+
+ // Use GetFullPath as an additional safety check
+ // for relative paths that contain a rooted path
+ // (e.g. "valid/path/C:\Windows\System.ini")
+ localPath = Path.GetFullPath(localPath);
+ }
+ catch
+ {
+ // Both IsPathRooted and GetFullPath throw exceptions
+ // if a path contains invalid characters or is otherwise invalid;
+ // bail out in this case too, as the path would not exist on disk anyway.
+ return null;
+ }
+#pragma warning restore CA1031
+
+ // As a final precaution, check that the resulting local path
+ // is inside the folder intended to be served.
+ if (!localPath.StartsWith(FileSystemPath, StringComparison.Ordinal))
+ return null;
+
+ if (File.Exists(localPath))
+ return GetMappedFileInfo(mimeTypeProvider, localPath);
+
+ if (Directory.Exists(localPath))
+ return GetMappedDirectoryInfo(localPath);
+
+ return null;
+ }
+
+ ///
+ public Stream OpenFile(string path) => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+
+ ///
+ public IEnumerable GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider)
+ => new DirectoryInfo(path).EnumerateFileSystemInfos()
+ .Select(fsi => GetMappedResourceInfo(mimeTypeProvider, fsi));
+
+ ///
+ /// Releases unmanaged and - optionally - managed resources.
+ ///
+ /// to release both managed and unmanaged resources;
+ /// to release only unmanaged resources.
+ protected virtual void Dispose(bool disposing)
+ {
+ ResourceChanged = null; // Release references to listeners
+
+ if (_watcher != null)
+ {
+ _watcher.EnableRaisingEvents = false;
+ _watcher.Changed -= Watcher_ChangedOrDeleted;
+ _watcher.Deleted -= Watcher_ChangedOrDeleted;
+ _watcher.Renamed -= Watcher_Renamed;
+
+ if (disposing)
+ _watcher.Dispose();
+ }
+ }
+
+ private static MappedResourceInfo GetMappedFileInfo(IMimeTypeProvider mimeTypeProvider, string localPath)
+ => GetMappedFileInfo(mimeTypeProvider, new FileInfo(localPath));
+
+ private static MappedResourceInfo GetMappedFileInfo(IMimeTypeProvider mimeTypeProvider, FileInfo info)
+ => MappedResourceInfo.ForFile(
+ info.FullName,
+ info.Name,
+ info.LastWriteTimeUtc,
+ info.Length,
+ mimeTypeProvider.GetMimeType(info.Extension));
+
+ private static MappedResourceInfo GetMappedDirectoryInfo(string localPath)
+ => GetMappedDirectoryInfo(new DirectoryInfo(localPath));
+
+ private static MappedResourceInfo GetMappedDirectoryInfo(DirectoryInfo info)
+ => MappedResourceInfo.ForDirectory(info.FullName, info.Name, info.LastWriteTimeUtc);
+
+ private static MappedResourceInfo GetMappedResourceInfo(IMimeTypeProvider mimeTypeProvider, FileSystemInfo info)
+ => info is DirectoryInfo directoryInfo
+ ? GetMappedDirectoryInfo(directoryInfo)
+ : GetMappedFileInfo(mimeTypeProvider, (FileInfo) info);
+
+ private void Watcher_ChangedOrDeleted(object sender, FileSystemEventArgs e)
+ => ResourceChanged?.Invoke(e.FullPath);
+
+ private void Watcher_Renamed(object sender, RenamedEventArgs e)
+ => ResourceChanged?.Invoke(e.OldFullPath);
+ }
+}
diff --git a/Vendor/EmbedIO-3.5.2/Files/IDirectoryLister.cs b/Vendor/EmbedIO-3.5.2/Files/IDirectoryLister.cs
new file mode 100644
index 0000000..0a2ba9a
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/IDirectoryLister.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace EmbedIO.Files
+{
+ ///
+ /// Represents an object that can render a directory listing to a stream.
+ ///
+ public interface IDirectoryLister
+ {
+ ///
+ /// Gets the MIME type of generated directory listings.
+ ///
+ string ContentType { get; }
+
+ ///
+ /// Asynchronously generate a directory listing.
+ ///
+ /// A containing information about
+ /// the directory which is to be listed.
+ /// The absolute URL path that was mapped to .
+ /// An enumeration of the entries in the directory represented by .
+ /// A to which the directory listing must be written.
+ /// A used to cancel the operation.
+ /// A representing the ongoing operation.
+ Task ListDirectoryAsync(
+ MappedResourceInfo info,
+ string absoluteUrlPath,
+ IEnumerable entries,
+ Stream stream,
+ CancellationToken cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/IFileProvider.cs b/Vendor/EmbedIO-3.5.2/Files/IFileProvider.cs
new file mode 100644
index 0000000..048265c
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/IFileProvider.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+
+namespace EmbedIO.Files
+{
+ ///
+ /// Represents an object that can provide files and/or directories to be served by a .
+ ///
+ public interface IFileProvider
+ {
+ ///
+ /// Occurs when a file or directory provided by this instance is modified or removed.
+ /// The event's parameter is the provider-specific path of the resource that changed.
+ ///
+ event Action ResourceChanged;
+
+ ///
+ /// Gets a value indicating whether the files and directories provided by this instance
+ /// will never change.
+ ///
+ bool IsImmutable { get; }
+
+ ///
+ /// Signals a file provider that the web server is starting.
+ ///
+ /// A used to stop the web server.
+ void Start(CancellationToken cancellationToken);
+
+ ///
+ /// Maps a URL path to a provider-specific path.
+ ///
+ /// The URL path.
+ /// An interface to use
+ /// for determining the MIME type of a file.
+ /// A provider-specific path identifying a file or directory,
+ /// or if this instance cannot provide a resource associated
+ /// to .
+ MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider);
+
+ ///
+ /// Opens a file for reading.
+ ///
+ /// The provider-specific path for the file.
+ ///
+ /// A readable of the file's contents.
+ ///
+ Stream OpenFile(string path);
+
+ ///
+ /// Returns an enumeration of the entries of a directory.
+ ///
+ /// The provider-specific path for the directory.
+ /// An interface to use
+ /// for determining the MIME type of files.
+ /// An enumeration of objects identifying the entries
+ /// in the directory identified by .
+ IEnumerable GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider);
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/Internal/Base64Utility.cs b/Vendor/EmbedIO-3.5.2/Files/Internal/Base64Utility.cs
new file mode 100644
index 0000000..19d1579
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/Internal/Base64Utility.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace EmbedIO.Files.Internal
+{
+ internal static class Base64Utility
+ {
+ // long is 8 bytes
+ // base64 of 8 bytes is 12 chars, but the last one is padding
+ public static string LongToBase64(long value)
+ => Convert.ToBase64String(BitConverter.GetBytes(value)).Substring(0, 11);
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/Internal/EntityTag.cs b/Vendor/EmbedIO-3.5.2/Files/Internal/EntityTag.cs
new file mode 100644
index 0000000..288d594
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/Internal/EntityTag.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Text;
+
+namespace EmbedIO.Files.Internal
+{
+ internal static class EntityTag
+ {
+ public static string Compute(DateTime lastModifiedUtc, long length, CompressionMethod compressionMethod)
+ {
+ var sb = new StringBuilder()
+ .Append('"')
+ .Append(Base64Utility.LongToBase64(lastModifiedUtc.Ticks))
+ .Append(Base64Utility.LongToBase64(length));
+
+ switch (compressionMethod)
+ {
+ case CompressionMethod.Deflate:
+ sb.Append('-').Append(CompressionMethodNames.Deflate);
+ break;
+ case CompressionMethod.Gzip:
+ sb.Append('-').Append(CompressionMethodNames.Gzip);
+ break;
+ }
+
+ return sb.Append('"').ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/Internal/FileCacheItem.cs b/Vendor/EmbedIO-3.5.2/Files/Internal/FileCacheItem.cs
new file mode 100644
index 0000000..78aa18e
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/Internal/FileCacheItem.cs
@@ -0,0 +1,164 @@
+using System;
+using EmbedIO.Internal;
+
+namespace EmbedIO.Files.Internal
+{
+ internal sealed class FileCacheItem
+ {
+#pragma warning disable SA1401 // Field should be private - performance is a stronger concern here.
+ // These fields create a sort of linked list of items
+ // inside the cache's dictionary.
+ // Their purpose is to keep track of items
+ // in order from least to most recently used.
+ internal string? PreviousKey;
+ internal string? NextKey;
+ internal long LastUsedAt;
+#pragma warning restore SA1401
+
+ // Size of a pointer in bytes
+ private static readonly long SizeOfPointer = Environment.Is64BitProcess ? 8 : 4;
+
+ // Size of a WeakReference in bytes
+ private static readonly long SizeOfWeakReference = Environment.Is64BitProcess ? 16 : 32;
+
+ // Educated guess about the size of an Item in memory (see comments on constructor).
+ // 3 * SizeOfPointer + total size of fields, rounded up to a multiple of 16.
+ //
+ // Computed as follows:
+ //
+ // * for 32-bit:
+ // - initialize count to 3 (number of "hidden" pointers that compose the object header)
+ // - for every field / auto property, in order of declaration:
+ // - increment count by 1 for reference types, 2 for long and DateTime
+ // (as of time of writing there are no fields of other types here)
+ // - increment again by 1 if this field "weighs" 1 and the next one "weighs" 2
+ // (padding for field alignment)
+ // - multiply count by 4 (size of a pointer)
+ // - if the result is not a multiple of 16, round it up to next multiple of 16
+ //
+ // * for 64-bit:
+ // - initialize count to 3 (number of "hidden" pointers that compose the object header)
+ // - for every field / auto property, in order of declaration, increment count by 1
+ // (at the time of writing there are no fields here that need padding on 64-bit)
+ // - multiply count by 8 (size of a pointer)
+ // - if the result is not a multiple of 16, round it up to next multiple of 16
+ private static readonly long SizeOfItem = Environment.Is64BitProcess ? 96 : 128;
+
+ private readonly object _syncRoot = new object();
+
+ // Used to update total size of section.
+ // Weak reference avoids circularity.
+ private readonly WeakReference _section;
+
+ // There are only 3 possible compression methods,
+ // hence a dictionary (or two dictionaries) would be overkill.
+ private byte[]? _uncompressedContent;
+ private byte[]? _gzippedContent;
+ private byte[]? _deflatedContent;
+
+ internal FileCacheItem(FileCache.Section section, DateTime lastModifiedUtc, long length)
+ {
+ _section = new WeakReference(section);
+
+ LastModifiedUtc = lastModifiedUtc;
+ Length = length;
+
+ // There is no way to know the actual size of an object at runtime.
+ // This method makes some educated guesses, based on the following
+ // article (among others):
+ // https://codingsight.com/precise-computation-of-clr-object-size/
+ // PreviousKey and NextKey values aren't counted in
+ // because they are just references to existing strings.
+ SizeInCache = SizeOfItem + SizeOfWeakReference;
+ }
+
+ public DateTime LastModifiedUtc { get; }
+
+ public long Length { get; }
+
+ // This is the (approximate) in-memory size of this object.
+ // It is NOT the length of the cache resource!
+ public long SizeInCache { get; private set; }
+
+ public byte[]? GetContent(CompressionMethod compressionMethod)
+ {
+ // If there are both entity tag and content, use them.
+ switch (compressionMethod)
+ {
+ case CompressionMethod.Deflate:
+ if (_deflatedContent != null) return _deflatedContent;
+ break;
+ case CompressionMethod.Gzip:
+ if (_gzippedContent != null) return _gzippedContent;
+ break;
+ default:
+ if (_uncompressedContent != null) return _uncompressedContent;
+ break;
+ }
+
+ // Try to convert existing content, if any.
+ byte[]? content;
+ if (_uncompressedContent != null)
+ {
+ content = CompressionUtility.ConvertCompression(_uncompressedContent, CompressionMethod.None, compressionMethod);
+ }
+ else if (_gzippedContent != null)
+ {
+ content = CompressionUtility.ConvertCompression(_gzippedContent, CompressionMethod.Gzip, compressionMethod);
+ }
+ else if (_deflatedContent != null)
+ {
+ content = CompressionUtility.ConvertCompression(_deflatedContent, CompressionMethod.Deflate, compressionMethod);
+ }
+ else
+ {
+ // No content whatsoever.
+ return null;
+ }
+
+ return SetContent(compressionMethod, content);
+ }
+
+ public byte[]? SetContent(CompressionMethod compressionMethod, byte[]? content)
+ {
+ // This is the bare minimum locking we need
+ // to ensure we don't mess sizes up.
+ byte[]? oldContent;
+ lock (_syncRoot)
+ {
+ switch (compressionMethod)
+ {
+ case CompressionMethod.Deflate:
+ oldContent = _deflatedContent;
+ _deflatedContent = content;
+ break;
+ case CompressionMethod.Gzip:
+ oldContent = _gzippedContent;
+ _gzippedContent = content;
+ break;
+ default:
+ oldContent = _uncompressedContent;
+ _uncompressedContent = content;
+ break;
+ }
+ }
+
+ var sizeDelta = GetSizeOf(content) - GetSizeOf(oldContent);
+ SizeInCache += sizeDelta;
+ if (_section.TryGetTarget(out var section))
+ section.UpdateTotalSize(sizeDelta);
+
+ return content;
+ }
+
+ // Round up to a multiple of 16
+ private static long RoundUpTo16(long n)
+ {
+ var remainder = n % 16;
+ return remainder > 0 ? n + (16 - remainder) : n;
+ }
+
+ // The size of a byte array is 3 * SizeOfPointer + 1 (size of byte) * Length
+ private static long GetSizeOf(byte[]? arr) => arr == null ? 0 : RoundUpTo16(3 * SizeOfPointer) + arr.Length;
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/Internal/HtmlDirectoryLister.cs b/Vendor/EmbedIO-3.5.2/Files/Internal/HtmlDirectoryLister.cs
new file mode 100644
index 0000000..f85eebf
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/Internal/HtmlDirectoryLister.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using EmbedIO.Utilities;
+using Swan;
+
+namespace EmbedIO.Files.Internal
+{
+ internal class HtmlDirectoryLister : IDirectoryLister
+ {
+ private static readonly Lazy LazyInstance = new Lazy(() => new HtmlDirectoryLister());
+
+ private HtmlDirectoryLister()
+ {
+ }
+
+ public static IDirectoryLister Instance => LazyInstance.Value;
+
+ public string ContentType { get; } = MimeType.Html + "; encoding=" + WebServer.DefaultEncoding.WebName;
+
+ public async Task ListDirectoryAsync(
+ MappedResourceInfo info,
+ string absoluteUrlPath,
+ IEnumerable entries,
+ Stream stream,
+ CancellationToken cancellationToken)
+ {
+ const int MaxEntryLength = 50;
+ const int SizeIndent = -20; // Negative for right alignment
+
+ if (!info.IsDirectory)
+ throw SelfCheck.Failure($"{nameof(HtmlDirectoryLister)}.{nameof(ListDirectoryAsync)} invoked with a file, not a directory.");
+
+ var encodedPath = WebUtility.HtmlEncode(absoluteUrlPath);
+ using var text = new StreamWriter(stream, WebServer.DefaultEncoding);
+ text.Write("Index of ");
+ text.Write(encodedPath);
+ text.Write("
Index of ");
+ text.Write(encodedPath);
+ text.Write("
");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/Internal/MappedResourceInfoExtensions.cs b/Vendor/EmbedIO-3.5.2/Files/Internal/MappedResourceInfoExtensions.cs
new file mode 100644
index 0000000..4d780d4
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/Internal/MappedResourceInfoExtensions.cs
@@ -0,0 +1,8 @@
+namespace EmbedIO.Files.Internal
+{
+ internal static class MappedResourceInfoExtensions
+ {
+ public static string GetEntityTag(this MappedResourceInfo @this, CompressionMethod compressionMethod)
+ => EntityTag.Compute(@this.LastModifiedUtc, @this.Length, compressionMethod);
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/MappedResourceInfo.cs b/Vendor/EmbedIO-3.5.2/Files/MappedResourceInfo.cs
new file mode 100644
index 0000000..914bc7a
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/MappedResourceInfo.cs
@@ -0,0 +1,80 @@
+using System;
+
+namespace EmbedIO.Files
+{
+ ///
+ /// Contains information about a resource served via an .
+ ///
+ public sealed class MappedResourceInfo
+ {
+ private MappedResourceInfo(string path, string name, DateTime lastModifiedUtc, long length, string? contentType)
+ {
+ Path = path;
+ Name = name;
+ LastModifiedUtc = lastModifiedUtc;
+ Length = length;
+ ContentType = contentType;
+ }
+
+ ///
+ /// Gets a value indicating whether this instance represents a directory.
+ ///
+ public bool IsDirectory => ContentType == null;
+
+ ///
+ /// Gets a value indicating whether this instance represents a file.
+ ///
+ public bool IsFile => ContentType != null;
+
+ ///
+ /// Gets a unique, provider-specific path for the resource.
+ ///
+ public string Path { get; }
+
+ ///
+ /// Gets the name of the resource, as it would appear in a directory listing.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the UTC date and time of the last modification made to the resource.
+ ///
+ public DateTime LastModifiedUtc { get; }
+
+ ///
+ /// If is , gets the length of the file, expressed in bytes.
+ /// If is , this property is always zero.
+ ///
+ public long Length { get; }
+
+ ///
+ /// If is , gets a MIME type describing the kind of contents of the file.
+ /// If is , this property is always .
+ ///
+ public string? ContentType { get; }
+
+ ///
+ /// Creates and returns a new instance of the class,
+ /// representing a file.
+ ///
+ /// A unique, provider-specific path for the file.
+ /// The name of the file, as it would appear in a directory listing.
+ /// The UTC date and time of the last modification made to the file.
+ /// The length of the file, expressed in bytes.
+ /// A MIME type describing the kind of contents of the file.
+ /// A newly-constructed instance of .
+ public static MappedResourceInfo ForFile(string path, string name, DateTime lastModifiedUtc, long size, string contentType)
+ => new MappedResourceInfo(path, name, lastModifiedUtc, size, contentType ?? MimeType.Default);
+
+ ///
+ /// Creates and returns a new instance of the class,
+ /// representing a directory.
+ ///
+ /// A unique, provider-specific path for the directory.
+ /// The name of the directory, as it would appear in a directory listing.
+ /// The UTC date and time of the last modification made to the directory.
+ /// A newly-constructed instance of .
+ public static MappedResourceInfo ForDirectory(string path, string name, DateTime lastModifiedUtc)
+ => new MappedResourceInfo(path, name, lastModifiedUtc, 0, null);
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/ResourceFileProvider.cs b/Vendor/EmbedIO-3.5.2/Files/ResourceFileProvider.cs
new file mode 100644
index 0000000..21e850a
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/ResourceFileProvider.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using EmbedIO.Utilities;
+
+namespace EmbedIO.Files
+{
+ ///
+ /// Provides access to embedded resources to a .
+ ///
+ ///
+ public class ResourceFileProvider : IFileProvider
+ {
+ private readonly DateTime _fileTime = DateTime.UtcNow;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The assembly where served files are contained as embedded resources.
+ /// A string to prepend to provider-specific paths
+ /// to form the name of a manifest resource in .
+ /// is .
+ public ResourceFileProvider(Assembly assembly, string pathPrefix)
+ {
+ Assembly = Validate.NotNull(nameof(assembly), assembly);
+ PathPrefix = pathPrefix ?? string.Empty;
+ }
+
+ ///
+ public event Action ResourceChanged
+ {
+ add { }
+ remove { }
+ }
+
+ ///
+ /// Gets the assembly where served files are contained as embedded resources.
+ ///
+ public Assembly Assembly { get; }
+
+ ///
+ /// Gets a string that is prepended to provider-specific paths to form the name of a manifest resource in .
+ ///
+ public string PathPrefix { get; }
+
+ ///
+ public bool IsImmutable => true;
+
+ ///
+ public void Start(CancellationToken cancellationToken)
+ {
+ }
+
+ ///
+ public MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider)
+ {
+ var resourceName = PathPrefix + urlPath.Replace('/', '.');
+
+ long size;
+ try
+ {
+ using var stream = Assembly.GetManifestResourceStream(resourceName);
+ if (stream == null || stream == Stream.Null)
+ return null;
+
+ size = stream.Length;
+ }
+ catch (FileNotFoundException)
+ {
+ return null;
+ }
+
+ var lastSlashPos = urlPath.LastIndexOf('/');
+ var name = urlPath.Substring(lastSlashPos + 1);
+
+ return MappedResourceInfo.ForFile(
+ resourceName,
+ name,
+ _fileTime,
+ size,
+ mimeTypeProvider.GetMimeType(Path.GetExtension(name)));
+ }
+
+ ///
+ public Stream OpenFile(string path) => Assembly.GetManifestResourceStream(path);
+
+ ///
+ public IEnumerable GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider)
+ => Enumerable.Empty();
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/Files/ZipFileProvider.cs b/Vendor/EmbedIO-3.5.2/Files/ZipFileProvider.cs
new file mode 100644
index 0000000..5a9c82d
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/Files/ZipFileProvider.cs
@@ -0,0 +1,110 @@
+using EmbedIO.Utilities;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Threading;
+
+namespace EmbedIO.Files
+{
+ ///
+ /// Provides access to files contained in a .zip file to a .
+ ///
+ ///
+ public class ZipFileProvider : IDisposable, IFileProvider
+ {
+ private readonly ZipArchive _zipArchive;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The zip file path.
+ public ZipFileProvider(string zipFilePath)
+ : this(new FileStream(Validate.LocalPath(nameof(zipFilePath), zipFilePath, true), FileMode.Open))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The stream that contains the archive.
+ /// to leave the stream open after the web server
+ /// is disposed; otherwise, .
+ public ZipFileProvider(Stream stream, bool leaveOpen = false)
+ {
+ _zipArchive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen);
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~ZipFileProvider()
+ {
+ Dispose(false);
+ }
+
+ ///
+ public event Action ResourceChanged
+ {
+ add { }
+ remove { }
+ }
+
+ ///
+ public bool IsImmutable => true;
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ public void Start(CancellationToken cancellationToken)
+ {
+ }
+
+ ///
+ public MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider)
+ {
+ if (urlPath.Length == 1)
+ return null;
+
+ urlPath = Uri.UnescapeDataString(urlPath);
+
+ var entry = _zipArchive.GetEntry(urlPath.Substring(1));
+ if (entry == null)
+ return null;
+
+ return MappedResourceInfo.ForFile(
+ entry.FullName,
+ entry.Name,
+ entry.LastWriteTime.DateTime,
+ entry.Length,
+ mimeTypeProvider.GetMimeType(Path.GetExtension(entry.Name)));
+ }
+
+ ///
+ public Stream OpenFile(string path)
+ => _zipArchive.GetEntry(path)?.Open() ?? throw new FileNotFoundException($"\"{path}\" cannot be found in Zip archive.");
+
+ ///
+ public IEnumerable GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider)
+ => Enumerable.Empty();
+
+ ///
+ /// Releases unmanaged and - optionally - managed resources.
+ ///
+ /// to release both managed and unmanaged resources;
+ /// to release only unmanaged resources.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposing)
+ return;
+
+ _zipArchive.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Items.cs b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Items.cs
new file mode 100644
index 0000000..d82f88f
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Items.cs
@@ -0,0 +1,47 @@
+using System;
+
+namespace EmbedIO
+{
+ partial class HttpContextExtensions
+ {
+ /// Gets the item associated with the specified key.
+ /// The desired type of the item.
+ /// The on which this method is called.
+ /// The key whose value to get from the Items dictionary.
+ ///
+ /// When this method returns, the item associated with the specified key,
+ /// if the key is found in Items
+ /// and the associated value is of type ;
+ /// otherwise, the default value for .
+ /// This parameter is passed uninitialized.
+ ///
+ /// if the item is found and is of type ;
+ /// otherwise, .
+ /// is .
+ /// is .
+ public static bool TryGetItem(this IHttpContext @this, object key, out T value)
+ {
+ if (@this.Items.TryGetValue(key, out var item) && item is T typedItem)
+ {
+ value = typedItem;
+ return true;
+ }
+
+#pragma warning disable CS8653 // value is non-nullable - We are returning false, so value is undefined.
+ value = default;
+#pragma warning restore CS8653
+ return false;
+ }
+
+ /// Gets the item associated with the specified key.
+ /// The desired type of the item.
+ /// The on which this method is called.
+ /// The key whose value to get from the Items dictionary.
+ /// The item associated with the specified key,
+ /// if the key is found in Items
+ /// and the associated value is of type ;
+ /// otherwise, the default value for .
+ public static T GetItem(this IHttpContext @this, object key)
+ => @this.Items.TryGetValue(key, out var item) && item is T typedItem ? typedItem : default;
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Redirect.cs b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Redirect.cs
new file mode 100644
index 0000000..5cc7ff9
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Redirect.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Net;
+using EmbedIO.Utilities;
+
+namespace EmbedIO
+{
+ partial class HttpContextExtensions
+ {
+ ///
+ /// Sets a redirection status code and adds a Location header to the response.
+ ///
+ /// The interface on which this method is called.
+ /// The URL to which the user agent should be redirected.
+ /// The status code to set on the response.
+ /// is .
+ /// is .
+ ///
+ /// is not a valid relative or absolute URL..
+ /// - or -
+ /// is not a redirection (3xx) status code.
+ ///
+ public static void Redirect(this IHttpContext @this, string location, int statusCode = (int)HttpStatusCode.Found)
+ {
+ location = Validate.Url(nameof(location), location, @this.Request.Url);
+
+ if (statusCode < 300 || statusCode > 399)
+ throw new ArgumentException("Redirect status code is not valid.", nameof(statusCode));
+
+ @this.Response.SetEmptyResponse(statusCode);
+ @this.Response.Headers[HttpHeaderNames.Location] = location;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpContextExtensions-RequestStream.cs b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-RequestStream.cs
new file mode 100644
index 0000000..9f9158e
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-RequestStream.cs
@@ -0,0 +1,61 @@
+using System.IO;
+using System.IO.Compression;
+using System.Text;
+using Swan.Logging;
+
+namespace EmbedIO
+{
+ partial class HttpContextExtensions
+ {
+ ///
+ /// Wraps the request input stream and returns a that can be used directly.
+ /// Decompression of compressed request bodies is implemented if specified in the web server's options.
+ ///
+ /// The on which this method is called.
+ ///
+ /// A that can be used to write response data.
+ /// This stream MUST be disposed when finished writing.
+ ///
+ ///
+ ///
+ public static Stream OpenRequestStream(this IHttpContext @this)
+ {
+ var stream = @this.Request.InputStream;
+
+ var encoding = @this.Request.Headers[HttpHeaderNames.ContentEncoding]?.Trim();
+ switch (encoding)
+ {
+ case CompressionMethodNames.Gzip:
+ if (@this.SupportCompressedRequests)
+ return new GZipStream(stream, CompressionMode.Decompress);
+ break;
+ case CompressionMethodNames.Deflate:
+ if (@this.SupportCompressedRequests)
+ return new DeflateStream(stream, CompressionMode.Decompress);
+ break;
+ case CompressionMethodNames.None:
+ case null:
+ return stream;
+ }
+
+ $"[{@this.Id}] Unsupported request content encoding \"{encoding}\", sending 400 Bad Request..."
+ .Warn(nameof(OpenRequestStream));
+
+ throw HttpException.BadRequest($"Unsupported content encoding \"{encoding}\"");
+ }
+
+ ///
+ /// Wraps the request input stream and returns a that can be used directly.
+ /// Decompression of compressed request bodies is implemented if specified in the web server's options.
+ ///
+ /// The on which this method is called.
+ ///
+ /// A that can be used to read the request body as text.
+ /// This reader MUST be disposed when finished reading.
+ ///
+ ///
+ ///
+ public static TextReader OpenRequestText(this IHttpContext @this)
+ => new StreamReader(OpenRequestStream(@this), @this.Request.ContentEncoding);
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Requests.cs b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Requests.cs
new file mode 100644
index 0000000..f3948ee
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Requests.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Collections.Specialized;
+using System.IO;
+using System.Threading.Tasks;
+using EmbedIO.Utilities;
+using Swan;
+
+namespace EmbedIO
+{
+ partial class HttpContextExtensions
+ {
+ private static readonly object FormDataKey = new object();
+ private static readonly object QueryDataKey = new object();
+
+ ///
+ /// Asynchronously retrieves the request body as an array of s.
+ ///
+ /// The on which this method is called.
+ /// A Task, representing the ongoing operation,
+ /// whose result will be an array of s containing the request body.
+ /// is .
+ public static async Task GetRequestBodyAsByteArrayAsync(this IHttpContext @this)
+ {
+ using var buffer = new MemoryStream();
+ using var stream = @this.OpenRequestStream();
+ await stream.CopyToAsync(buffer, WebServer.StreamCopyBufferSize, @this.CancellationToken).ConfigureAwait(false);
+ return buffer.ToArray();
+ }
+
+ ///
+ /// Asynchronously buffers the request body into a read-only .
+ ///
+ /// The on which this method is called.
+ /// A Task, representing the ongoing operation,
+ /// whose result will be a read-only containing the request body.
+ /// is .
+ public static async Task GetRequestBodyAsMemoryStreamAsync(this IHttpContext @this)
+ => new MemoryStream(
+ await GetRequestBodyAsByteArrayAsync(@this).ConfigureAwait(false),
+ false);
+
+ ///
+ /// Asynchronously retrieves the request body as a string.
+ ///
+ /// The on which this method is called.
+ /// A Task, representing the ongoing operation,
+ /// whose result will be a representation of the request body.
+ /// is .
+ public static async Task GetRequestBodyAsStringAsync(this IHttpContext @this)
+ {
+ using var reader = @this.OpenRequestText();
+ return await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously deserializes a request body, using the default request deserializer.
+ /// As of EmbedIO version 3.0, the default response serializer has the same behavior of JSON
+ /// request parsing methods of version 2.
+ ///
+ /// The expected type of the deserialized data.
+ /// The on which this method is called.
+ /// A Task, representing the ongoing operation,
+ /// whose result will be the deserialized data.
+ /// is .
+ public static Task GetRequestDataAsync(this IHttpContext @this)
+ => RequestDeserializer.Default(@this);
+
+ ///
+ /// Asynchronously deserializes a request body, using the specified request deserializer.
+ ///
+ /// The expected type of the deserialized data.
+ /// The on which this method is called.
+ /// A used to deserialize the request body.
+ /// A Task, representing the ongoing operation,
+ /// whose result will be the deserialized data.
+ /// is .
+ /// is .
+ public static Task GetRequestDataAsync(this IHttpContext @this,RequestDeserializerCallback deserializer)
+ => Validate.NotNull(nameof(deserializer), deserializer)(@this);
+
+ ///
+ /// Asynchronously parses a request body in application/x-www-form-urlencoded format.
+ ///
+ /// The on which this method is called.
+ /// A Task, representing the ongoing operation,
+ /// whose result will be a read-only of form field names and values.
+ /// is .
+ ///
+ /// This method may safely be called more than once for the same :
+ /// it will return the same collection instead of trying to parse the request body again.
+ ///
+ public static async Task GetRequestFormDataAsync(this IHttpContext @this)
+ {
+ if (!@this.Items.TryGetValue(FormDataKey, out var previousResult))
+ {
+ NameValueCollection result;
+ try
+ {
+ using var reader = @this.OpenRequestText();
+ result = UrlEncodedDataParser.Parse(await reader.ReadToEndAsync().ConfigureAwait(false), false);
+ }
+ catch (Exception e)
+ {
+ @this.Items[FormDataKey] = e;
+ throw;
+ }
+
+ @this.Items[FormDataKey] = result;
+ return result;
+ }
+
+ switch (previousResult)
+ {
+ case NameValueCollection collection:
+ return collection;
+
+ case Exception exception:
+ throw exception.RethrowPreservingStackTrace();
+
+ case null:
+ throw SelfCheck.Failure($"Previous result of {nameof(HttpContextExtensions)}.{nameof(GetRequestFormDataAsync)} is null.");
+
+ default:
+ throw SelfCheck.Failure($"Previous result of {nameof(HttpContextExtensions)}.{nameof(GetRequestFormDataAsync)} is of unexpected type {previousResult.GetType().FullName}");
+ }
+ }
+
+ ///
+ /// Parses a request URL query. Note that this is different from getting the property,
+ /// in that fields without an equal sign are treated as if they have an empty value, instead of their keys being grouped
+ /// as values of the null key.
+ ///
+ /// The on which this method is called.
+ /// A read-only .
+ /// is .
+ ///
+ /// This method may safely be called more than once for the same :
+ /// it will return the same collection instead of trying to parse the request body again.
+ ///
+ public static NameValueCollection GetRequestQueryData(this IHttpContext @this)
+ {
+ if (!@this.Items.TryGetValue(QueryDataKey, out var previousResult))
+ {
+ NameValueCollection result;
+ try
+ {
+ result = UrlEncodedDataParser.Parse(@this.Request.Url.Query, false);
+ }
+ catch (Exception e)
+ {
+ @this.Items[FormDataKey] = e;
+ throw;
+ }
+
+ @this.Items[FormDataKey] = result;
+ return result;
+ }
+
+ switch (previousResult)
+ {
+ case NameValueCollection collection:
+ return collection;
+
+ case Exception exception:
+ throw exception.RethrowPreservingStackTrace();
+
+ case null:
+ throw SelfCheck.Failure($"Previous result of {nameof(HttpContextExtensions)}.{nameof(GetRequestQueryData)} is null.");
+
+ default:
+ throw SelfCheck.Failure($"Previous result of {nameof(HttpContextExtensions)}.{nameof(GetRequestQueryData)} is of unexpected type {previousResult.GetType().FullName}");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpContextExtensions-ResponseStream.cs b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-ResponseStream.cs
new file mode 100644
index 0000000..59dd4be
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-ResponseStream.cs
@@ -0,0 +1,68 @@
+using System.IO;
+using System.IO.Compression;
+using System.Text;
+using EmbedIO.Internal;
+
+namespace EmbedIO
+{
+ partial class HttpContextExtensions
+ {
+ ///
+ /// Wraps the response output stream and returns a that can be used directly.
+ /// Optional buffering is applied, so that the response may be sent as one instead of using chunked transfer.
+ /// Proactive negotiation is performed to select the best compression method supported by the client.
+ ///
+ /// The on which this method is called.
+ /// If set to , sent data is collected
+ /// in a and sent all at once when the returned
+ /// is disposed; if set to (the default), chunked transfer will be used.
+ /// if sending compressed data is preferred over
+ /// sending non-compressed data; otherwise, .
+ ///
+ /// A that can be used to write response data.
+ /// This stream MUST be disposed when finished writing.
+ ///
+ ///
+ public static Stream OpenResponseStream(this IHttpContext @this, bool buffered = false, bool preferCompression = true)
+ {
+ // No need to check whether negotiation is successful;
+ // the returned callback will throw HttpNotAcceptableException if it was not.
+ _ = @this.Request.TryNegotiateContentEncoding(preferCompression, out var compressionMethod, out var prepareResponse);
+ prepareResponse(@this.Response);
+ var stream = buffered ? new BufferingResponseStream(@this.Response) : @this.Response.OutputStream;
+
+ return compressionMethod switch {
+ CompressionMethod.Gzip => new GZipStream(stream, CompressionMode.Compress),
+ CompressionMethod.Deflate => new DeflateStream(stream, CompressionMode.Compress),
+ _ => stream
+ };
+ }
+
+ ///
+ /// Wraps the response output stream and returns a that can be used directly.
+ /// Optional buffering is applied, so that the response may be sent as one instead of using chunked transfer.
+ /// Proactive negotiation is performed to select the best compression method supported by the client.
+ ///
+ /// The on which this method is called.
+ ///
+ /// The to use to convert text to data bytes.
+ /// If (the default), (UTF-8 without a byte order mark) is used.
+ ///
+ /// If set to , sent data is collected
+ /// in a and sent all at once when the returned
+ /// is disposed; if set to (the default), chunked transfer will be used.
+ /// if sending compressed data is preferred over
+ /// sending non-compressed data; otherwise, .
+ ///
+ /// A that can be used to write response data.
+ /// This writer MUST be disposed when finished writing.
+ ///
+ ///
+ public static TextWriter OpenResponseText(this IHttpContext @this, Encoding? encoding = null, bool buffered = false, bool preferCompression = true)
+ {
+ encoding ??= WebServer.DefaultEncoding;
+ @this.Response.ContentEncoding = encoding;
+ return new StreamWriter(OpenResponseStream(@this, buffered, preferCompression), encoding);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Responses.cs b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Responses.cs
new file mode 100644
index 0000000..3447ad4
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Responses.cs
@@ -0,0 +1,124 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using EmbedIO.Utilities;
+
+namespace EmbedIO
+{
+ partial class HttpContextExtensions
+ {
+ private const string StandardHtmlHeaderFormat = "{0} - {1}
{0} - {1}
";
+ private const string StandardHtmlFooter = "";
+
+ ///
+ /// Asynchronously sends a string as response.
+ ///
+ /// The interface on which this method is called.
+ /// The response content.
+ /// The MIME type of the content. If , the content type will not be set.
+ /// The to use.
+ /// A representing the ongoing operation.
+ /// is .
+ ///
+ /// is .
+ /// - or -
+ /// is .
+ ///
+ public static async Task SendStringAsync(
+ this IHttpContext @this,
+ string content,
+ string contentType,
+ Encoding encoding)
+ {
+ content = Validate.NotNull(nameof(content), content);
+ encoding = Validate.NotNull(nameof(encoding), encoding);
+
+ if (contentType != null)
+ {
+ @this.Response.ContentType = contentType;
+ @this.Response.ContentEncoding = encoding;
+ }
+
+ using var text = @this.OpenResponseText(encoding);
+ await text.WriteAsync(content).ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously sends a standard HTML response for the specified status code.
+ ///
+ /// The interface on which this method is called.
+ /// The HTTP status code of the response.
+ /// A representing the ongoing operation.
+ /// is .
+ /// There is no standard status description for .
+ ///
+ public static Task SendStandardHtmlAsync(this IHttpContext @this, int statusCode)
+ => SendStandardHtmlAsync(@this, statusCode, null);
+
+ ///
+ /// Asynchronously sends a standard HTML response for the specified status code.
+ ///
+ /// The interface on which this method is called.
+ /// The HTTP status code of the response.
+ /// A callback function that may write additional HTML code
+ /// to a representing the response output.
+ /// If not , the callback is called immediately before closing the HTML body tag.
+ /// A representing the ongoing operation.
+ /// is .
+ /// There is no standard status description for .
+ ///
+ public static Task SendStandardHtmlAsync(
+ this IHttpContext @this,
+ int statusCode,
+ Action? writeAdditionalHtml)
+ {
+ if (!HttpStatusDescription.TryGet(statusCode, out var statusDescription))
+ throw new ArgumentException("Status code has no standard description.", nameof(statusCode));
+
+ @this.Response.StatusCode = statusCode;
+ @this.Response.StatusDescription = statusDescription;
+ @this.Response.ContentType = MimeType.Html;
+ @this.Response.ContentEncoding = WebServer.DefaultEncoding;
+ using (var text = @this.OpenResponseText(WebServer.DefaultEncoding))
+ {
+ text.Write(StandardHtmlHeaderFormat, statusCode, statusDescription, WebServer.DefaultEncoding.WebName);
+ writeAdditionalHtml?.Invoke(text);
+ text.Write(StandardHtmlFooter);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Asynchronously sends serialized data as a response, using the default response serializer.
+ /// As of EmbedIO version 3.0, the default response serializer has the same behavior of JSON
+ /// response methods of version 2.
+ ///
+ /// The interface on which this method is called.
+ /// The data to serialize.
+ /// A representing the ongoing operation.
+ /// is .
+ ///
+ ///
+ public static Task SendDataAsync(this IHttpContext @this, object data)
+ => ResponseSerializer.Default(@this, data);
+
+ ///
+ /// Asynchronously sends serialized data as a response, using the specified response serializer.
+ /// As of EmbedIO version 3.0, the default response serializer has the same behavior of JSON
+ /// response methods of version 2.
+ ///
+ /// The interface on which this method is called.
+ /// A used to prepare the response.
+ /// The data to serialize.
+ /// A representing the ongoing operation.
+ /// is .
+ /// is .
+ ///
+ ///
+ public static Task SendDataAsync(this IHttpContext @this, ResponseSerializerCallback serializer, object data)
+ => Validate.NotNull(nameof(serializer), serializer)(@this, data);
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpContextExtensions.cs b/Vendor/EmbedIO-3.5.2/HttpContextExtensions.cs
new file mode 100644
index 0000000..46da20c
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpContextExtensions.cs
@@ -0,0 +1,30 @@
+using System;
+using EmbedIO.Utilities;
+using Swan;
+
+namespace EmbedIO
+{
+ ///
+ /// Provides extension methods for types implementing .
+ ///
+ public static partial class HttpContextExtensions
+ {
+ ///
+ /// Gets the underlying interface of an .
+ /// This API mainly supports the EmbedIO infrastructure; it is not intended to be used directly from your code,
+ /// unless to fulfill very specific needs in the development of plug-ins (modules, etc.) for EmbedIO.
+ ///
+ /// The interface on which this method is called.
+ /// The underlying interface representing
+ /// the HTTP context implementation.
+ ///
+ /// is .
+ ///
+ ///
+ /// does not implement .
+ ///
+ public static IHttpContextImpl GetImplementation(this IHttpContext @this)
+ => Validate.NotNull(nameof(@this), @this) as IHttpContextImpl
+ ?? throw SelfCheck.Failure($"{@this.GetType().FullName} does not implement {nameof(IHttpContextImpl)}.");
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpException-Shortcuts.cs b/Vendor/EmbedIO-3.5.2/HttpException-Shortcuts.cs
new file mode 100644
index 0000000..7c8c50b
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpException-Shortcuts.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Net;
+
+namespace EmbedIO
+{
+ partial class HttpException
+ {
+ ///
+ /// Returns a new instance of that, when thrown,
+ /// will break the request handling control flow and send a 500 Internal Server Error
+ /// response to the client.
+ ///
+ /// A message to include in the response.
+ /// The data object to include in the response.
+ ///
+ /// A newly-created .
+ ///
+ public static HttpException InternalServerError(string? message = null, object? data = null)
+ => new HttpException(HttpStatusCode.InternalServerError, message, data);
+
+ ///
+ /// Returns a new instance of that, when thrown,
+ /// will break the request handling control flow and send a 401 Unauthorized
+ /// response to the client.
+ ///
+ /// A message to include in the response.
+ /// The data object to include in the response.
+ ///
+ /// A newly-created .
+ ///
+ public static HttpException Unauthorized(string? message = null, object? data = null)
+ => new HttpException(HttpStatusCode.Unauthorized, message, data);
+
+ ///
+ /// Returns a new instance of that, when thrown,
+ /// will break the request handling control flow and send a 403 Forbidden
+ /// response to the client.
+ ///
+ /// A message to include in the response.
+ /// The data object to include in the response.
+ /// A newly-created .
+ public static HttpException Forbidden(string? message = null, object? data = null)
+ => new HttpException(HttpStatusCode.Forbidden, message, data);
+
+ ///
+ /// Returns a new instance of that, when thrown,
+ /// will break the request handling control flow and send a 400 Bad Request
+ /// response to the client.
+ ///
+ /// A message to include in the response.
+ /// The data object to include in the response.
+ /// A newly-created .
+ public static HttpException BadRequest(string? message = null, object? data = null)
+ => new HttpException(HttpStatusCode.BadRequest, message, data);
+
+ ///
+ /// Returns a new instance of that, when thrown,
+ /// will break the request handling control flow and send a 404 Not Found
+ /// response to the client.
+ ///
+ /// A message to include in the response.
+ /// The data object to include in the response.
+ /// A newly-created .
+ public static HttpException NotFound(string? message = null, object? data = null)
+ => new HttpException(HttpStatusCode.NotFound, message, data);
+
+ ///
+ /// Returns a new instance of that, when thrown,
+ /// will break the request handling control flow and send a 405 Method Not Allowed
+ /// response to the client.
+ ///
+ /// A message to include in the response.
+ /// The data object to include in the response.
+ /// A newly-created .
+ public static HttpException MethodNotAllowed(string? message = null, object? data = null)
+ => new HttpException(HttpStatusCode.MethodNotAllowed, message, data);
+
+ ///
+ /// Returns a new instance of that, when thrown,
+ /// will break the request handling control flow and send a 406 Not Acceptable
+ /// response to the client.
+ ///
+ /// A newly-created .
+ ///
+ public static HttpNotAcceptableException NotAcceptable() => new HttpNotAcceptableException();
+
+ ///
+ /// Returns a new instance of that, when thrown,
+ /// will break the request handling control flow and send a 406 Not Acceptable
+ /// response to the client.
+ ///
+ /// A value, or a comma-separated list of values, to set the response's Vary header to.
+ /// A newly-created .
+ ///
+ public static HttpNotAcceptableException NotAcceptable(string vary) => new HttpNotAcceptableException(vary);
+
+ ///
+ /// Returns a new instance of that, when thrown,
+ /// will break the request handling control flow and send a 416 Range Not Satisfiable
+ /// response to the client.
+ ///
+ /// A newly-created .
+ ///
+ public static HttpRangeNotSatisfiableException RangeNotSatisfiable() => new HttpRangeNotSatisfiableException();
+
+ ///
+ /// Returns a new instance of that, when thrown,
+ /// will break the request handling control flow and send a 416 Range Not Satisfiable
+ /// response to the client.
+ ///
+ /// The total length of the requested resource, expressed in bytes,
+ /// or to omit the Content-Range header in the response.
+ /// A newly-created .
+ ///
+ public static HttpRangeNotSatisfiableException RangeNotSatisfiable(long? contentLength)
+ => new HttpRangeNotSatisfiableException(contentLength);
+
+ ///
+ /// Returns a new instance of that, when thrown,
+ /// will break the request handling control flow and redirect the client
+ /// to the specified location, using response status code 302.
+ ///
+ /// The redirection target.
+ ///
+ /// A newly-created .
+ ///
+ public static HttpRedirectException Redirect(string location)
+ => new HttpRedirectException(location);
+
+ ///
+ /// Returns a new instance of that, when thrown,
+ /// will break the request handling control flow and redirect the client
+ /// to the specified location, using the specified response status code.
+ ///
+ /// The redirection target.
+ /// The status code to set on the response, in the range from 300 to 399.
+ ///
+ /// A newly-created .
+ ///
+ /// is not in the 300-399 range.
+ public static HttpRedirectException Redirect(string location, int statusCode)
+ => new HttpRedirectException(location, statusCode);
+
+ ///
+ /// Returns a new instance of that, when thrown,
+ /// will break the request handling control flow and redirect the client
+ /// to the specified location, using the specified response status code.
+ ///
+ /// The redirection target.
+ /// One of the redirection status codes, to be set on the response.
+ ///
+ /// A newly-created .
+ ///
+ /// is not a redirection status code.
+ public static HttpRedirectException Redirect(string location, HttpStatusCode statusCode)
+ => new HttpRedirectException(location, statusCode);
+ }
+}
diff --git a/Vendor/EmbedIO-3.5.2/HttpException.cs b/Vendor/EmbedIO-3.5.2/HttpException.cs
new file mode 100644
index 0000000..be3e635
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpException.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Net;
+
+namespace EmbedIO
+{
+ ///
+ /// When thrown, breaks the request handling control flow
+ /// and sends an error response to the client.
+ ///
+#pragma warning disable CA1032 // Implement standard exception constructors - they have no meaning here.
+ public partial class HttpException : Exception, IHttpException
+#pragma warning restore CA1032
+ {
+ ///
+ /// Initializes a new instance of the class,
+ /// with no message to include in the response.
+ ///
+ /// The status code to set on the response.
+ public HttpException(int statusCode)
+ {
+ StatusCode = statusCode;
+ }
+
+ ///
+ /// Initializes a new instance of the class,
+ /// with no message to include in the response.
+ ///
+ /// The status code to set on the response.
+ public HttpException(HttpStatusCode statusCode)
+ : this((int)statusCode)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class,
+ /// with a message to include in the response.
+ ///
+ /// The status code to set on the response.
+ /// A message to include in the response as plain text.
+ public HttpException(int statusCode, string? message)
+ : base(message)
+ {
+ StatusCode = statusCode;
+ HttpExceptionMessage = message;
+ }
+
+ ///
+ /// Initializes a new instance of the class,
+ /// with a message to include in the response.
+ ///
+ /// The status code to set on the response.
+ /// A message to include in the response as plain text.
+ public HttpException(HttpStatusCode statusCode, string? message)
+ : this((int)statusCode, message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class,
+ /// with a message and a data object to include in the response.
+ ///
+ /// The status code to set on the response.
+ /// A message to include in the response as plain text.
+ /// The data object to include in the response.
+ public HttpException(int statusCode, string? message, object? data)
+ : this(statusCode, message)
+ {
+ DataObject = data;
+ }
+
+ ///
+ /// Initializes a new instance of the class,
+ /// with a message and a data object to include in the response.
+ ///
+ /// The status code to set on the response.
+ /// A message to include in the response as plain text.
+ /// The data object to include in the response.
+ public HttpException(HttpStatusCode statusCode, string? message, object? data)
+ : this((int)statusCode, message, data)
+ {
+ }
+
+ ///
+ public int StatusCode { get; }
+
+ ///
+ public object? DataObject { get; }
+
+ ///
+ string? IHttpException.Message => HttpExceptionMessage;
+
+ // This property is necessary because when an exception with a null Message is thrown
+ // the CLR provides a standard message. We want null to remain null in IHttpException.
+ private string? HttpExceptionMessage { get; }
+
+ ///
+ ///
+ /// This method does nothing; there is no need to call
+ /// base.PrepareResponse in overrides of this method.
+ ///
+ public virtual void PrepareResponse(IHttpContext context)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpExceptionHandler.cs b/Vendor/EmbedIO-3.5.2/HttpExceptionHandler.cs
new file mode 100644
index 0000000..b72efa8
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpExceptionHandler.cs
@@ -0,0 +1,153 @@
+using System;
+using System.Net;
+using System.Runtime.ExceptionServices;
+using System.Net.Http;
+using System.Threading.Tasks;
+using System.Web;
+using EmbedIO.Utilities;
+using Swan.Logging;
+
+namespace EmbedIO
+{
+ ///
+ /// Provides standard handlers for HTTP exceptions at both module and server level.
+ ///
+ ///
+ /// Where applicable, HTTP exception handlers defined in this class
+ /// use the and
+ /// properties to customize
+ /// their behavior.
+ ///
+ ///
+ ///
+ public static class HttpExceptionHandler
+ {
+ ///
+ /// Gets the default handler used by .
+ /// This is the same as .
+ ///
+ public static HttpExceptionHandlerCallback Default { get; } = HtmlResponse;
+
+ ///
+ /// Sends an empty response.
+ ///
+ /// An interface representing the context of the request.
+ /// The HTTP exception.
+ /// A representing the ongoing operation.
+#pragma warning disable CA1801 // Unused parameter
+ public static Task EmptyResponse(IHttpContext context, IHttpException httpException)
+#pragma warning restore CA1801
+ => Task.CompletedTask;
+
+ ///
+ /// Sends a HTTP exception's Message property
+ /// as a plain text response.
+ /// This handler does not use the DataObject property.
+ ///
+ /// An interface representing the context of the request.
+ /// The HTTP exception.
+ /// A representing the ongoing operation.
+ public static Task PlainTextResponse(IHttpContext context, IHttpException httpException)
+ => context.SendStringAsync(httpException.Message ?? string.Empty, MimeType.PlainText, WebServer.DefaultEncoding);
+
+ ///
+ /// Sends a response with a HTML payload
+ /// briefly describing the error, including contact information and/or a stack trace
+ /// if specified via the
+ /// and properties, respectively.
+ /// This handler does not use the DataObject property.
+ ///
+ /// An interface representing the context of the request.
+ /// The HTTP exception.
+ /// A representing the ongoing operation.
+ public static Task HtmlResponse(IHttpContext context, IHttpException httpException)
+ => context.SendStandardHtmlAsync(
+ httpException.StatusCode,
+ text => {
+ text.Write(
+ "
If this error is completely unexpected to you, and you think you should not seeing this page, please contact the server administrator");
+
+ if (!string.IsNullOrEmpty(ExceptionHandler.ContactInformation))
+ text.Write(" ({0})", WebUtility.HtmlEncode(ExceptionHandler.ContactInformation));
+
+ text.Write(", informing them of the time this error occurred and the action(s) you performed that resulted in this error.
",
+ WebUtility.HtmlEncode(httpException.StackTrace));
+ }
+ });
+
+ ///
+ /// Gets a that will serialize a HTTP exception's
+ /// DataObject property and send it as a JSON response.
+ ///
+ /// A used to serialize data and send it to the client.
+ /// A .
+ /// is .
+ public static HttpExceptionHandlerCallback DataResponse(ResponseSerializerCallback serializerCallback)
+ {
+ Validate.NotNull(nameof(serializerCallback), serializerCallback);
+
+ return (context, httpException) => serializerCallback(context, httpException.DataObject);
+ }
+
+ ///
+ /// Gets a that will serialize a HTTP exception's
+ /// Message and DataObject properties
+ /// and send them as a JSON response.
+ /// The response will be a JSON object with a message property and a data property.
+ ///
+ /// A used to serialize data and send it to the client.
+ /// A .
+ /// is .
+ public static HttpExceptionHandlerCallback FullDataResponse(ResponseSerializerCallback serializerCallback)
+ {
+ Validate.NotNull(nameof(serializerCallback), serializerCallback);
+
+ return (context, httpException) => serializerCallback(context, new
+ {
+ message = httpException.Message,
+ data = httpException.DataObject,
+ });
+ }
+
+ internal static async Task Handle(string logSource, IHttpContext context, Exception exception, HttpExceptionHandlerCallback? handler)
+ {
+ if (handler == null || !(exception is IHttpException httpException))
+ {
+ ExceptionDispatchInfo.Capture(exception).Throw();
+ return;
+ }
+
+ exception.Log(logSource, $"[{context.Id}] HTTP exception {httpException.StatusCode}");
+
+ try
+ {
+ context.Response.SetEmptyResponse(httpException.StatusCode);
+ context.Response.DisableCaching();
+ httpException.PrepareResponse(context);
+ await handler(context, httpException)
+ .ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested)
+ {
+ throw;
+ }
+ catch (HttpListenerException)
+ {
+ throw;
+ }
+ catch (Exception exception2)
+ {
+ exception2.Log(logSource, $"[{context.Id}] Unhandled exception while handling HTTP exception {httpException.StatusCode}");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpExceptionHandlerCallback.cs b/Vendor/EmbedIO-3.5.2/HttpExceptionHandlerCallback.cs
new file mode 100644
index 0000000..488f6d7
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpExceptionHandlerCallback.cs
@@ -0,0 +1,21 @@
+using System.Threading.Tasks;
+
+namespace EmbedIO
+{
+ ///
+ /// A callback used to build the contents of the response for an .
+ ///
+ /// An interface representing the context of the request.
+ /// An interface.
+ /// A representing the ongoing operation.
+ ///
+ /// When this delegate is called, the response's status code has already been set and the
+ /// method has already been called. The only thing left to do is preparing the response's content, according
+ /// to the property.
+ /// Any exception thrown by a handler (even a HTTP exception) will go unhandled: the web server
+ /// will not crash, but processing of the request will be aborted, and the response will be flushed as-is.
+ /// In other words, it is not a good ides to throw HttpException.NotFound() (or similar)
+ /// from a handler.
+ ///
+ public delegate Task HttpExceptionHandlerCallback(IHttpContext context, IHttpException httpException);
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpHeaderNames.cs b/Vendor/EmbedIO-3.5.2/HttpHeaderNames.cs
new file mode 100644
index 0000000..a29846f
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpHeaderNames.cs
@@ -0,0 +1,449 @@
+namespace EmbedIO
+{
+ ///
+ /// Exposes known HTTP header names.
+ ///
+ ///
+ /// The constants in this class have been extracted from a list of known HTTP header names.
+ /// The presence of a header name in this class is not a guarantee that EmbedIO supports,
+ /// or even recognizes, it. Refer to the documentation for each module for information about supported
+ /// headers.
+ ///
+ public static class HttpHeaderNames
+ {
+ // The .NET Core sources were taken as reference for this list of constants.
+ // See https://github.com/dotnet/corefx/blob/master/src/Common/src/System/Net/HttpKnownHeaderNames.cs
+ // However, not all constants come from there, so be careful not to copy-paste indiscriminately.
+
+ ///
+ /// The Accept HTTP header.
+ ///
+ public const string Accept = "Accept";
+
+ ///
+ /// The Accept-Charset HTTP header.
+ ///
+ public const string AcceptCharset = "Accept-Charset";
+
+ ///
+ /// The Accept-Encoding HTTP header.
+ ///
+ public const string AcceptEncoding = "Accept-Encoding";
+
+ ///
+ /// The Accept-Language HTTP header.
+ ///
+ public const string AcceptLanguage = "Accept-Language";
+
+ ///
+ /// The Accept-Patch HTTP header.
+ ///
+ public const string AcceptPatch = "Accept-Patch";
+
+ ///
+ /// The Accept-Ranges HTTP header.
+ ///
+ public const string AcceptRanges = "Accept-Ranges";
+
+ ///
+ /// The Access-Control-Allow-Credentials HTTP header.
+ ///
+ public const string AccessControlAllowCredentials = "Access-Control-Allow-Credentials";
+
+ ///
+ /// The Access-Control-Allow-Headers HTTP header.
+ ///
+ public const string AccessControlAllowHeaders = "Access-Control-Allow-Headers";
+
+ ///
+ /// The Access-Control-Allow-Methods HTTP header.
+ ///
+ public const string AccessControlAllowMethods = "Access-Control-Allow-Methods";
+
+ ///
+ /// The Access-Control-Allow-Origin HTTP header.
+ ///
+ public const string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
+
+ ///
+ /// The Access-Control-Expose-Headers HTTP header.
+ ///
+ public const string AccessControlExposeHeaders = "Access-Control-Expose-Headers";
+
+ ///
+ /// The Access-Control-Max-Age HTTP header.
+ ///
+ public const string AccessControlMaxAge = "Access-Control-Max-Age";
+
+ ///
+ /// The Access-Control-Request-Headers HTTP header.
+ ///
+ public const string AccessControlRequestHeaders = "Access-Control-Request-Headers";
+
+ ///
+ /// The Access-Control-Request-Method HTTP header.
+ ///
+ public const string AccessControlRequestMethod = "Access-Control-Request-Method";
+
+ ///
+ /// The Age HTTP header.
+ ///
+ public const string Age = "Age";
+
+ ///
+ /// The Allow HTTP header.
+ ///
+ public const string Allow = "Allow";
+
+ ///
+ /// The Alt-Svc HTTP header.
+ ///
+ public const string AltSvc = "Alt-Svc";
+
+ ///
+ /// The Authorization HTTP header.
+ ///
+ public const string Authorization = "Authorization";
+
+ ///
+ /// The Cache-Control HTTP header.
+ ///
+ public const string CacheControl = "Cache-Control";
+
+ ///
+ /// The Connection HTTP header.
+ ///
+ public const string Connection = "Connection";
+
+ ///
+ /// The Content-Disposition HTTP header.
+ ///
+ public const string ContentDisposition = "Content-Disposition";
+
+ ///
+ /// The Content-Encoding HTTP header.
+ ///
+ public const string ContentEncoding = "Content-Encoding";
+
+ ///
+ /// The Content-Language HTTP header.
+ ///
+ public const string ContentLanguage = "Content-Language";
+
+ ///
+ /// The Content-Length HTTP header.
+ ///
+ public const string ContentLength = "Content-Length";
+
+ ///
+ /// The Content-Location HTTP header.
+ ///
+ public const string ContentLocation = "Content-Location";
+
+ ///
+ /// The Content-MD5 HTTP header.
+ ///
+ public const string ContentMD5 = "Content-MD5";
+
+ ///
+ /// The Content-Range HTTP header.
+ ///
+ public const string ContentRange = "Content-Range";
+
+ ///
+ /// The Content-Security-Policy HTTP header.
+ ///
+ public const string ContentSecurityPolicy = "Content-Security-Policy";
+
+ ///
+ /// The Content-Type HTTP header.
+ ///
+ public const string ContentType = "Content-Type";
+
+ ///
+ /// The Cookie HTTP header.
+ ///
+ public const string Cookie = "Cookie";
+
+ ///
+ /// The Cookie2 HTTP header.
+ ///
+ public const string Cookie2 = "Cookie2";
+
+ ///
+ /// The Date HTTP header.
+ ///
+ public const string Date = "Date";
+
+ ///
+ /// The ETag HTTP header.
+ ///
+ public const string ETag = "ETag";
+
+ ///
+ /// The Expect HTTP header.
+ ///
+ public const string Expect = "Expect";
+
+ ///
+ /// The Expires HTTP header.
+ ///
+ public const string Expires = "Expires";
+
+ ///
+ /// The From HTTP header.
+ ///
+ public const string From = "From";
+
+ ///
+ /// The Host HTTP header.
+ ///
+ public const string Host = "Host";
+
+ ///
+ /// The If-Match HTTP header.
+ ///
+ public const string IfMatch = "If-Match";
+
+ ///
+ /// The If-Modified-Since HTTP header.
+ ///
+ public const string IfModifiedSince = "If-Modified-Since";
+
+ ///
+ /// The If-None-Match HTTP header.
+ ///
+ public const string IfNoneMatch = "If-None-Match";
+
+ ///
+ /// The If-Range HTTP header.
+ ///
+ public const string IfRange = "If-Range";
+
+ ///
+ /// The If-Unmodified-Since HTTP header.
+ ///
+ public const string IfUnmodifiedSince = "If-Unmodified-Since";
+
+ ///
+ /// The Keep-Alive HTTP header.
+ ///
+ public const string KeepAlive = "Keep-Alive";
+
+ ///
+ /// The Last-Modified HTTP header.
+ ///
+ public const string LastModified = "Last-Modified";
+
+ ///
+ /// The Link HTTP header.
+ ///
+ public const string Link = "Link";
+
+ ///
+ /// The Location HTTP header.
+ ///
+ public const string Location = "Location";
+
+ ///
+ /// The Max-Forwards HTTP header.
+ ///
+ public const string MaxForwards = "Max-Forwards";
+
+ ///
+ /// The Origin HTTP header.
+ ///
+ public const string Origin = "Origin";
+
+ ///
+ /// The P3P HTTP header.
+ ///
+ public const string P3P = "P3P";
+
+ ///
+ /// The Pragma HTTP header.
+ ///
+ public const string Pragma = "Pragma";
+
+ ///
+ /// The Proxy-Authenticate HTTP header.
+ ///
+ public const string ProxyAuthenticate = "Proxy-Authenticate";
+
+ ///
+ /// The Proxy-Authorization HTTP header.
+ ///
+ public const string ProxyAuthorization = "Proxy-Authorization";
+
+ ///
+ /// The Proxy-Connection HTTP header.
+ ///
+ public const string ProxyConnection = "Proxy-Connection";
+
+ ///
+ /// The Public-Key-Pins HTTP header.
+ ///
+ public const string PublicKeyPins = "Public-Key-Pins";
+
+ ///
+ /// The Range HTTP header.
+ ///
+ public const string Range = "Range";
+
+ ///
+ /// The Referer HTTP header.
+ ///
+ ///
+ /// The incorrect spelling ("Referer" instead of "Referrer") is intentional
+ /// and has historical reasons.
+ /// See the "Etymology" section of the Wikipedia article
+ /// on this header for more information.
+ ///
+ public const string Referer = "Referer";
+
+ ///
+ /// The Retry-After HTTP header.
+ ///
+ public const string RetryAfter = "Retry-After";
+
+ ///
+ /// The Sec-WebSocket-Accept HTTP header.
+ ///
+ public const string SecWebSocketAccept = "Sec-WebSocket-Accept";
+
+ ///
+ /// The Sec-WebSocket-Extensions HTTP header.
+ ///
+ public const string SecWebSocketExtensions = "Sec-WebSocket-Extensions";
+
+ ///
+ /// The Sec-WebSocket-Key HTTP header.
+ ///
+ public const string SecWebSocketKey = "Sec-WebSocket-Key";
+
+ ///
+ /// The Sec-WebSocket-Protocol HTTP header.
+ ///
+ public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol";
+
+ ///
+ /// The Sec-WebSocket-Version HTTP header.
+ ///
+ public const string SecWebSocketVersion = "Sec-WebSocket-Version";
+
+ ///
+ /// The Server HTTP header.
+ ///
+ public const string Server = "Server";
+
+ ///
+ /// The Set-Cookie HTTP header.
+ ///
+ public const string SetCookie = "Set-Cookie";
+
+ ///
+ /// The Set-Cookie2 HTTP header.
+ ///
+ public const string SetCookie2 = "Set-Cookie2";
+
+ ///
+ /// The Strict-Transport-Security HTTP header.
+ ///
+ public const string StrictTransportSecurity = "Strict-Transport-Security";
+
+ ///
+ /// The TE HTTP header.
+ ///
+ public const string TE = "TE";
+
+ ///
+ /// The TSV HTTP header.
+ ///
+ public const string TSV = "TSV";
+
+ ///
+ /// The Trailer HTTP header.
+ ///
+ public const string Trailer = "Trailer";
+
+ ///
+ /// The Transfer-Encoding HTTP header.
+ ///
+ public const string TransferEncoding = "Transfer-Encoding";
+
+ ///
+ /// The Upgrade HTTP header.
+ ///
+ public const string Upgrade = "Upgrade";
+
+ ///
+ /// The Upgrade-Insecure-Requests HTTP header.
+ ///
+ public const string UpgradeInsecureRequests = "Upgrade-Insecure-Requests";
+
+ ///
+ /// The User-Agent HTTP header.
+ ///
+ public const string UserAgent = "User-Agent";
+
+ ///
+ /// The Vary HTTP header.
+ ///
+ public const string Vary = "Vary";
+
+ ///
+ /// The Via HTTP header.
+ ///
+ public const string Via = "Via";
+
+ ///
+ /// The WWW-Authenticate HTTP header.
+ ///
+ public const string WWWAuthenticate = "WWW-Authenticate";
+
+ ///
+ /// The Warning HTTP header.
+ ///
+ public const string Warning = "Warning";
+
+ ///
+ /// The X-AspNet-Version HTTP header.
+ ///
+ public const string XAspNetVersion = "X-AspNet-Version";
+
+ ///
+ /// The X-Content-Duration HTTP header.
+ ///
+ public const string XContentDuration = "X-Content-Duration";
+
+ ///
+ /// The X-Content-Type-Options HTTP header.
+ ///
+ public const string XContentTypeOptions = "X-Content-Type-Options";
+
+ ///
+ /// The X-Frame-Options HTTP header.
+ ///
+ public const string XFrameOptions = "X-Frame-Options";
+
+ ///
+ /// The X-MSEdge-Ref HTTP header.
+ ///
+ public const string XMSEdgeRef = "X-MSEdge-Ref";
+
+ ///
+ /// The X-Powered-By HTTP header.
+ ///
+ public const string XPoweredBy = "X-Powered-By";
+
+ ///
+ /// The X-Request-ID HTTP header.
+ ///
+ public const string XRequestID = "X-Request-ID";
+
+ ///
+ /// The X-UA-Compatible HTTP header.
+ ///
+ public const string XUACompatible = "X-UA-Compatible";
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpListenerMode.cs b/Vendor/EmbedIO-3.5.2/HttpListenerMode.cs
new file mode 100644
index 0000000..3e10107
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpListenerMode.cs
@@ -0,0 +1,20 @@
+namespace EmbedIO
+{
+ ///
+ /// Defines the HTTP listeners available for use in a .
+ ///
+ public enum HttpListenerMode
+ {
+ ///
+ /// Use EmbedIO's internal HTTP listener implementation,
+ /// based on Mono's System.Net.HttpListener.
+ ///
+ EmbedIO,
+
+ ///
+ /// Use the class
+ /// provided by the .NET runtime in use.
+ ///
+ Microsoft,
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpNotAcceptableException.cs b/Vendor/EmbedIO-3.5.2/HttpNotAcceptableException.cs
new file mode 100644
index 0000000..f84f47e
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpNotAcceptableException.cs
@@ -0,0 +1,55 @@
+using System.Net;
+
+namespace EmbedIO
+{
+ ///
+ /// When thrown, breaks the request handling control flow
+ /// and sends a redirection response to the client.
+ ///
+#pragma warning disable CA1032 // Implement standard exception constructors - they have no meaning here.
+ public class HttpNotAcceptableException : HttpException
+#pragma warning restore CA1032
+ {
+ ///
+ /// Initializes a new instance of the class,
+ /// without specifying a value for the response's Vary header.
+ ///
+ public HttpNotAcceptableException()
+ : this(null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// A value, or a comma-separated list of values, to set the response's Vary header to.
+ /// Although not specified in RFC7231,
+ /// this may help the client to understand why the request has been rejected.
+ /// If this parameter is or the empty string, the response's Vary header
+ /// is not set.
+ ///
+ public HttpNotAcceptableException(string? vary)
+ : base((int)HttpStatusCode.NotAcceptable)
+ {
+ Vary = string.IsNullOrEmpty(vary) ? null : vary;
+ }
+
+ ///
+ /// Gets the value, or comma-separated list of values, to be set
+ /// on the response's Vary header.
+ ///
+ ///
+ /// If the empty string has been passed to the
+ /// constructor, the value of this property is .
+ ///
+ public string? Vary { get; }
+
+ ///
+ public override void PrepareResponse(IHttpContext context)
+ {
+ if (Vary != null)
+ context.Response.Headers.Add(HttpHeaderNames.Vary, Vary);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpRangeNotSatisfiableException.cs b/Vendor/EmbedIO-3.5.2/HttpRangeNotSatisfiableException.cs
new file mode 100644
index 0000000..8397e1b
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpRangeNotSatisfiableException.cs
@@ -0,0 +1,50 @@
+using System.Net;
+
+namespace EmbedIO
+{
+ ///
+ /// When thrown, breaks the request handling control flow
+ /// and sends a redirection response to the client.
+ ///
+#pragma warning disable CA1032 // Implement standard exception constructors - they have no meaning here.
+ public class HttpRangeNotSatisfiableException : HttpException
+#pragma warning restore CA1032
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// without specifying a value for the response's Content-Range header.
+ ///
+ public HttpRangeNotSatisfiableException()
+ : this(null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The total length of the requested resource, expressed in bytes,
+ /// or to omit the Content-Range header in the response.
+ public HttpRangeNotSatisfiableException(long? contentLength)
+ : base((int)HttpStatusCode.RequestedRangeNotSatisfiable)
+ {
+ ContentLength = contentLength;
+ }
+
+ ///
+ /// Gets the total content length to be specified
+ /// on the response's Content-Range header.
+ ///
+ public long? ContentLength { get; }
+
+ ///
+ public override void PrepareResponse(IHttpContext context)
+ {
+ // RFC 7233, Section 3.1: "When this status code is generated in response
+ // to a byte-range request, the sender
+ // SHOULD generate a Content-Range header field specifying
+ // the current length of the selected representation."
+ if (ContentLength.HasValue)
+ context.Response.Headers.Set(HttpHeaderNames.ContentRange, $"bytes */{ContentLength.Value}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpRedirectException.cs b/Vendor/EmbedIO-3.5.2/HttpRedirectException.cs
new file mode 100644
index 0000000..22b6801
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpRedirectException.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Net;
+
+namespace EmbedIO
+{
+ ///
+ /// When thrown, breaks the request handling control flow
+ /// and sends a redirection response to the client.
+ ///
+#pragma warning disable CA1032 // Implement standard exception constructors - they have no meaning here.
+ public class HttpRedirectException : HttpException
+#pragma warning restore CA1032
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The redirection target.
+ ///
+ /// The status code to set on the response, in the range from 300 to 399.
+ /// By default, status code 302 (Found) is used.
+ ///
+ /// is not in the 300-399 range.
+ public HttpRedirectException(string location, int statusCode = (int)HttpStatusCode.Found)
+ : base(statusCode)
+ {
+ if (statusCode < 300 || statusCode > 399)
+ throw new ArgumentException("Redirect status code is not valid.", nameof(statusCode));
+
+ Location = location;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The redirection target.
+ /// One of the redirection status codes, to be set on the response.
+ /// is not a redirection status code.
+ public HttpRedirectException(string location, HttpStatusCode statusCode)
+ : this(location, (int)statusCode)
+ {
+ }
+
+ ///
+ /// Gets the URL where the client will be redirected.
+ ///
+ public string Location { get; }
+
+ ///
+ public override void PrepareResponse(IHttpContext context)
+ {
+ context.Redirect(Location, StatusCode);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpRequestExtensions.cs b/Vendor/EmbedIO-3.5.2/HttpRequestExtensions.cs
new file mode 100644
index 0000000..70e55aa
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpRequestExtensions.cs
@@ -0,0 +1,262 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Net.Http.Headers;
+using EmbedIO.Utilities;
+
+namespace EmbedIO
+{
+ ///
+ /// Provides extension methods for types implementing .
+ ///
+ public static class HttpRequestExtensions
+ {
+ ///
+ /// Returns a string representing the remote IP address and port of an interface.
+ /// This method can be called even on a interface, or one that has no
+ /// remote end point, or no remote address; it will always return a non-,
+ /// non-empty string.
+ ///
+ /// The on which this method is called.
+ ///
+ /// If is , or its RemoteEndPoint
+ /// is , the string "<null>; otherwise, the remote end point's
+ /// Address (or the string "<???>" if it is )
+ /// followed by a colon and the Port number.
+ ///
+ public static string SafeGetRemoteEndpointStr(this IHttpRequest @this)
+ {
+ var endPoint = @this?.RemoteEndPoint;
+ return endPoint == null
+ ? ""
+ : $"{endPoint.Address?.ToString() ?? "??>"}:{endPoint.Port.ToString(CultureInfo.InvariantCulture)}";
+ }
+
+ ///
+ /// Attempts to proactively negotiate a compression method for a response,
+ /// based on a request's Accept-Encoding header (or lack of it).
+ ///
+ /// The on which this method is called.
+ /// if sending compressed data is preferred over
+ /// sending non-compressed data; otherwise, .
+ /// When this method returns, the compression method to use for the response,
+ /// if content negotiation is successful. This parameter is passed uninitialized.
+ /// When this method returns, a callback that prepares data in an
+ /// according to the result of content negotiation. This parameter is passed uninitialized.
+ /// if content negotiation is successful;
+ /// otherwise, .
+ ///
+ /// If this method returns , the callback
+ /// will set appropriate response headers to reflect the results of content negotiation.
+ /// If this method returns , the callback
+ /// will throw a to send a 406 Not Acceptable response
+ /// with the Vary header set to Accept-Encoding,
+ /// so that the client may know the reason why the request has been rejected.
+ /// If has noAccept-Encoding header, this method
+ /// always returns and sets
+ /// to .
+ ///
+ ///
+ public static bool TryNegotiateContentEncoding(
+ this IHttpRequest @this,
+ bool preferCompression,
+ out CompressionMethod compressionMethod,
+ out Action prepareResponse)
+ {
+ var acceptedEncodings = new QValueList(true, @this.Headers.GetValues(HttpHeaderNames.AcceptEncoding));
+ if (!acceptedEncodings.TryNegotiateContentEncoding(preferCompression, out compressionMethod, out var compressionMethodName))
+ {
+ prepareResponse = r => throw HttpException.NotAcceptable(HttpHeaderNames.AcceptEncoding);
+ return false;
+ }
+
+ prepareResponse = r => {
+ r.Headers.Add(HttpHeaderNames.Vary, HttpHeaderNames.AcceptEncoding);
+ r.Headers.Set(HttpHeaderNames.ContentEncoding, compressionMethodName);
+ };
+ return true;
+ }
+
+ ///
+ /// Checks whether an If-None-Match header exists in a request
+ /// and, if so, whether it contains a given entity tag.
+ /// See RFC7232, Section 3.2
+ /// for a normative reference; however, see the Remarks section for more information
+ /// about the RFC compliance of this method.
+ ///
+ /// The on which this method is called.
+ /// The entity tag.
+ /// When this method returns, a value that indicates whether an
+ /// If-None-Match header is present in , regardless of the method's
+ /// return value. This parameter is passed uninitialized.
+ /// if an If-None-Match header is present in
+ /// and one of the entity tags listed in it is equal to ;
+ /// otherwise.
+ ///
+ /// RFC7232, Section 3.2
+ /// states that a weak comparison function (as defined in
+ /// RFC7232, Section 2.3.2)
+ /// must be used for If-None-Match. That would mean parsing every entity tag, at least minimally,
+ /// to determine whether it is a "weak" or "strong" tag. Since EmbedIO currently generates only
+ /// "strong" tags, this method uses the default string comparer instead.
+ /// The behavior of this method is thus not, strictly speaking, RFC7232-compliant;
+ /// it works, though, with entity tags generated by EmbedIO.
+ ///
+ public static bool CheckIfNoneMatch(this IHttpRequest @this, string entityTag, out bool headerExists)
+ {
+ var values = @this.Headers.GetValues(HttpHeaderNames.IfNoneMatch);
+ if (values == null)
+ {
+ headerExists = false;
+ return false;
+ }
+
+ headerExists = true;
+ return values.Select(t => t.Trim()).Contains(entityTag);
+ }
+
+ // Check whether the If-Modified-Since request header exists
+ // and specifies a date and time more recent than or equal to
+ // the date and time of last modification of the requested resource.
+ // RFC7232, Section 3.3
+
+ ///
+ /// Checks whether an If-Modified-Since header exists in a request
+ /// and, if so, whether its value is a date and time more recent or equal to
+ /// a given .
+ /// See RFC7232, Section 3.3
+ /// for a normative reference.
+ ///
+ /// The on which this method is called.
+ /// A date and time value, in Coordinated Universal Time,
+ /// expressing the last time a resource was modified.
+ /// When this method returns, a value that indicates whether an
+ /// If-Modified-Since header is present in , regardless of the method's
+ /// return value. This parameter is passed uninitialized.
+ /// if an If-Modified-Since header is present in
+ /// and its value is a date and time more recent or equal to ;
+ /// otherwise.
+ public static bool CheckIfModifiedSince(this IHttpRequest @this, DateTime lastModifiedUtc, out bool headerExists)
+ {
+ var value = @this.Headers.Get(HttpHeaderNames.IfModifiedSince);
+ if (value == null)
+ {
+ headerExists = false;
+ return false;
+ }
+
+ headerExists = true;
+ return HttpDate.TryParse(value, out var dateTime)
+ && dateTime.UtcDateTime >= lastModifiedUtc;
+ }
+
+ // Checks the Range request header to tell whether to send
+ // a "206 Partial Content" response.
+
+ ///
+ /// Checks whether a Range header exists in a request
+ /// and, if so, determines whether it is possible to send a 206 Partial Content response.
+ /// See RFC7233
+ /// for a normative reference; however, see the Remarks section for more information
+ /// about the RFC compliance of this method.
+ ///
+ /// The on which this method is called.
+ /// The total length, in bytes, of the response entity, i.e.
+ /// what would be sent in a 200 OK response.
+ /// An entity tag representing the response entity. This value is checked against
+ /// the If-Range header, if it is present.
+ /// The date and time value, in Coordinated Universal Time,
+ /// expressing the last modification time of the resource entity. This value is checked against
+ /// the If-Range header, if it is present.
+ /// When this method returns , the start of the requested byte range.
+ /// This parameter is passed uninitialized.
+ ///
+ /// When this method returns , the upper bound of the requested byte range.
+ /// This parameter is passed uninitialized.
+ /// Note that the upper bound of a range is NOT the sum of the range's start and length;
+ /// for example, a range expressed as bytes=0-99 has a start of 0, an upper bound of 99,
+ /// and a length of 100 bytes.
+ ///
+ ///
+ /// This method returns if the following conditions are satisfied:
+ ///
+ /// >the request's HTTP method is GET;
+ /// >a Range header is present in the request;
+ /// >either no If-Range header is present in the request, or it
+ /// specifies an entity tag equal to , or a UTC date and time
+ /// equal to ;
+ /// >the Range header specifies exactly one range;
+ /// >the specified range is entirely contained in the range from 0 to - 1.
+ ///
+ /// If the last condition is not satisfied, i.e. the specified range start and/or upper bound
+ /// are out of the range from 0 to - 1, this method does not return;
+ /// it throws a instead.
+ /// If any of the other conditions are not satisfied, this method returns .
+ ///
+ ///
+ /// According to RFC7233, Section 3.1,
+ /// there are several conditions under which a server may ignore or reject a range request; therefore,
+ /// clients are (or should be) prepared to receive a 200 OK response with the whole response
+ /// entity instead of the requested range(s). For this reason, until the generation of
+ /// multipart/byteranges responses is implemented in EmbedIO, this method will ignore
+ /// range requests specifying more than one range, even if this behavior is not, strictly speaking,
+ /// RFC7233-compliant.
+ /// To make clients aware that range requests are accepted for a resource, every 200 OK
+ /// (or 304 Not Modified) response for the same resource should include an Accept-Ranges
+ /// header with the string bytes as value.
+ ///
+ public static bool IsRangeRequest(this IHttpRequest @this, long contentLength, string entityTag, DateTime lastModifiedUtc, out long start, out long upperBound)
+ {
+ start = 0;
+ upperBound = contentLength - 1;
+
+ // RFC7233, Section 3.1:
+ // "A server MUST ignore a Range header field received with a request method other than GET."
+ if (@this.HttpVerb != HttpVerbs.Get)
+ return false;
+
+ // No Range header, no partial content.
+ var rangeHeader = @this.Headers.Get(HttpHeaderNames.Range);
+ if (rangeHeader == null)
+ return false;
+
+ // Ignore the Range header if there is no If-Range header
+ // or if the If-Range header specifies a non-matching validator.
+ // RFC7233, Section 3.2: "If the validator given in the If-Range header field matches the
+ // current validator for the selected representation of the target
+ // resource, then the server SHOULD process the Range header field as
+ // requested.If the validator does not match, the server MUST ignore
+ // the Range header field.Note that this comparison by exact match,
+ // including when the validator is an HTTP-date, differs from the
+ // "earlier than or equal to" comparison used when evaluating an
+ // If-Unmodified-Since conditional."
+ var ifRange = @this.Headers.Get(HttpHeaderNames.IfRange)?.Trim();
+ if (ifRange != null && ifRange != entityTag)
+ {
+ if (!HttpDate.TryParse(ifRange, out var rangeDate))
+ return false;
+
+ if (rangeDate.UtcDateTime != lastModifiedUtc)
+ return false;
+ }
+
+ // Ignore the Range request header if it cannot be parsed successfully.
+ if (!RangeHeaderValue.TryParse(rangeHeader, out var range))
+ return false;
+
+ // EmbedIO does not support multipart/byteranges responses (yet),
+ // thus ignore range requests that specify one range.
+ if (range.Ranges.Count != 1)
+ return false;
+
+ var firstRange = range.Ranges.First();
+ start = firstRange.From ?? 0L;
+ upperBound = firstRange.To ?? contentLength - 1;
+ if (start >= contentLength || upperBound < start || upperBound >= contentLength)
+ throw HttpException.RangeNotSatisfiable(contentLength);
+
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpResponseExtensions.cs b/Vendor/EmbedIO-3.5.2/HttpResponseExtensions.cs
new file mode 100644
index 0000000..ad4c6c4
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpResponseExtensions.cs
@@ -0,0 +1,43 @@
+using System;
+using EmbedIO.Utilities;
+
+namespace EmbedIO
+{
+ ///
+ /// Provides extension methods for types implementing .
+ ///
+ public static class HttpResponseExtensions
+ {
+ ///
+ /// Sets the necessary headers to disable caching of a response on the client side.
+ ///
+ /// The interface on which this method is called.
+ /// is .
+ public static void DisableCaching(this IHttpResponse @this)
+ {
+ var headers = @this.Headers;
+ headers.Set(HttpHeaderNames.Expires, "Sat, 26 Jul 1997 05:00:00 GMT");
+ headers.Set(HttpHeaderNames.LastModified, HttpDate.Format(DateTime.UtcNow));
+ headers.Set(HttpHeaderNames.CacheControl, "no-store, no-cache, must-revalidate");
+ headers.Add(HttpHeaderNames.Pragma, "no-cache");
+ }
+
+ ///
+ /// Prepares a standard response without a body for the specified status code.
+ ///
+ /// The interface on which this method is called.
+ /// The HTTP status code of the response.
+ /// is .
+ /// There is no standard status description for .
+ public static void SetEmptyResponse(this IHttpResponse @this, int statusCode)
+ {
+ if (!HttpStatusDescription.TryGet(statusCode, out var statusDescription))
+ throw new ArgumentException("Status code has no standard description.", nameof(statusCode));
+
+ @this.StatusCode = statusCode;
+ @this.StatusDescription = statusDescription;
+ @this.ContentType = MimeType.Default;
+ @this.ContentEncoding = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpStatusDescription.cs b/Vendor/EmbedIO-3.5.2/HttpStatusDescription.cs
new file mode 100644
index 0000000..a724005
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpStatusDescription.cs
@@ -0,0 +1,146 @@
+using System.Collections.Generic;
+using System.Net;
+
+namespace EmbedIO
+{
+ ///
+ /// Provides standard HTTP status descriptions.
+ /// Data contained in this class comes from the following sources:
+ ///
+ /// RFC7231 Section 6 (HTTP/1.1 Semantics and Content)
+ /// RFC6585 (Additional HTTP Status Codes)
+ /// RFC2774 Section 7 (An HTTP Extension Framework)
+ /// RFC7540 Section 9.1.2 (HTTP/2)
+ /// RFC4918 Section 11 (WebDAV)
+ /// RFC5842 Section 7 (Binding Extensions to WebDAV)
+ /// RFC7538 Section 3 (HTTP Status Code 308)
+ /// RFC3229 Section 10.4.1 (Delta encoding in HTTP)
+ /// RFC8297 Section 2 (Early Hints)
+ /// RFC7725 Section 3 (HTTP-status-451)
+ /// RFC2295 Section 8.1 (Transparent Content Negotiation)
+ ///
+ ///
+ public static class HttpStatusDescription
+ {
+ private static readonly IReadOnlyDictionary Dictionary = new Dictionary {
+ { 100, "Continue" },
+ { 101, "Switching Protocols" },
+ { 102, "Processing" },
+ { 103, "Early Hints" },
+ { 200, "OK" },
+ { 201, "Created" },
+ { 202, "Accepted" },
+ { 203, "Non-Authoritative Information" },
+ { 204, "No Content" },
+ { 205, "Reset Content" },
+ { 206, "Partial Content" },
+ { 207, "Multi-Status" },
+ { 208, "Already Reported" },
+ { 226, "IM Used" },
+ { 300, "Multiple Choices" },
+ { 301, "Moved Permanently" },
+ { 302, "Found" },
+ { 303, "See Other" },
+ { 304, "Not Modified" },
+ { 305, "Use Proxy" },
+ { 307, "Temporary Redirect" },
+ { 308, "Permanent Redirect" },
+ { 400, "Bad Request" },
+ { 401, "Unauthorized" },
+ { 402, "Payment Required" },
+ { 403, "Forbidden" },
+ { 404, "Not Found" },
+ { 405, "Method Not Allowed" },
+ { 406, "Not Acceptable" },
+ { 407, "Proxy Authentication Required" },
+ { 408, "Request Timeout" },
+ { 409, "Conflict" },
+ { 410, "Gone" },
+ { 411, "Length Required" },
+ { 412, "Precondition Failed" },
+ { 413, "Request Entity Too Large" },
+ { 414, "Request-Uri Too Long" },
+ { 415, "Unsupported Media Type" },
+ { 416, "Requested Range Not Satisfiable" },
+ { 417, "Expectation Failed" },
+ { 421, "Misdirected Request" },
+ { 422, "Unprocessable Entity" },
+ { 423, "Locked" },
+ { 424, "Failed Dependency" },
+ { 426, "Upgrade Required" },
+ { 428, "Precondition Required" },
+ { 429, "Too Many Requests" },
+ { 431, "Request Header Fields Too Large" },
+ { 451, "Unavailable For Legal Reasons" },
+ { 500, "Internal Server Error" },
+ { 501, "Not Implemented" },
+ { 502, "Bad Gateway" },
+ { 503, "Service Unavailable" },
+ { 504, "Gateway Timeout" },
+ { 505, "Http Version Not Supported" },
+ { 506, "Variant Also Negotiates" },
+ { 507, "Insufficient Storage" },
+ { 508, "Loop Detected" },
+ { 510, "Not Extended" },
+ { 511, "Network Authentication Required" },
+ };
+
+ ///
+ /// Attempts to get the standard status description for a .
+ ///
+ /// The HTTP status code for which the standard description
+ /// is to be retrieved.
+ /// When this method returns, the standard HTTP status description
+ /// for the specified if it was found, or
+ /// if it was not found. This parameter is passed uninitialized.
+ /// if the specified was found
+ /// in the list of HTTP status codes for which the standard description is known;
+ /// otherwise, .
+ ///
+ ///
+ public static bool TryGet(HttpStatusCode code, out string description) => Dictionary.TryGetValue((int)code, out description);
+
+ ///
+ /// Attempts to get the standard status description for a HTTP status code
+ /// specified as an .
+ ///
+ /// The HTTP status code for which the standard description
+ /// is to be retrieved.
+ /// When this method returns, the standard HTTP status description
+ /// for the specified if it was found, or
+ /// if it was not found. This parameter is passed uninitialized.
+ /// if the specified was found
+ /// in the list of HTTP status codes for which the standard description is known;
+ /// otherwise, .
+ ///
+ ///
+ public static bool TryGet(int code, out string description) => Dictionary.TryGetValue(code, out description);
+
+ ///
+ /// Returns the standard status description for a .
+ ///
+ /// The HTTP status code for which the standard description
+ /// is to be retrieved.
+ /// The standard HTTP status description for the specified
+ /// if it was found, or if it was not found.
+ public static string Get(HttpStatusCode code)
+ {
+ Dictionary.TryGetValue((int)code, out var description);
+ return description;
+ }
+
+ ///
+ /// Returns the standard status description for a HTTP status code
+ /// specified as an .
+ ///
+ /// The HTTP status code for which the standard description
+ /// is to be retrieved.
+ /// The standard HTTP status description for the specified
+ /// if it was found, or if it was not found.
+ public static string Get(int code)
+ {
+ Dictionary.TryGetValue(code, out var description);
+ return description;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/HttpVerbs.cs b/Vendor/EmbedIO-3.5.2/HttpVerbs.cs
new file mode 100644
index 0000000..765b01e
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/HttpVerbs.cs
@@ -0,0 +1,48 @@
+namespace EmbedIO
+{
+ ///
+ /// Enumerates the different HTTP Verbs.
+ ///
+ public enum HttpVerbs
+ {
+ ///
+ /// Wildcard Method
+ ///
+ Any,
+
+ ///
+ /// DELETE Method
+ ///
+ Delete,
+
+ ///
+ /// GET Method
+ ///
+ Get,
+
+ ///
+ /// HEAD method
+ ///
+ Head,
+
+ ///
+ /// OPTIONS method
+ ///
+ Options,
+
+ ///
+ /// PATCH method
+ ///
+ Patch,
+
+ ///
+ /// POST method
+ ///
+ Post,
+
+ ///
+ /// PUT method
+ ///
+ Put,
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/ICookieCollection.cs b/Vendor/EmbedIO-3.5.2/ICookieCollection.cs
new file mode 100644
index 0000000..48f4f8f
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/ICookieCollection.cs
@@ -0,0 +1,49 @@
+using System.Net;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace EmbedIO
+{
+ ///
+ /// Interface for Cookie Collection.
+ ///
+ ///
+#pragma warning disable CA1010 // Should implement ICollection - not possible when wrapping System.Net.CookieCollection.
+ public interface ICookieCollection : IEnumerable, ICollection
+#pragma warning restore CA1010
+ {
+ ///
+ /// Gets the with the specified name.
+ ///
+ ///
+ /// The .
+ ///
+ /// The name.
+ /// The cookie matching the specified name.
+ Cookie? this[string name] { get; }
+
+ ///
+ /// Determines whether this contains the specified .
+ ///
+ /// The cookie to find in the .
+ ///
+ /// if this contains the specified ;
+ /// otherwise, .
+ ///
+ bool Contains(Cookie cookie);
+
+ ///
+ /// Copies the elements of this to a array
+ /// starting at the specified index of the target array.
+ ///
+ /// The target array to which the will be copied.
+ /// The zero-based index in the target where copying begins.
+ void CopyTo(Cookie[] array, int index);
+
+ ///
+ /// Adds the specified cookie.
+ ///
+ /// The cookie.
+ void Add(Cookie cookie);
+ }
+}
\ No newline at end of file
diff --git a/Vendor/EmbedIO-3.5.2/IHttpContext.cs b/Vendor/EmbedIO-3.5.2/IHttpContext.cs
new file mode 100644
index 0000000..bed7695
--- /dev/null
+++ b/Vendor/EmbedIO-3.5.2/IHttpContext.cs
@@ -0,0 +1,128 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Security.Principal;
+using System.Threading;
+using System.Threading.Tasks;
+using EmbedIO.Routing;
+using EmbedIO.Sessions;
+
+namespace EmbedIO
+{
+ ///
+ /// Represents the context of a HTTP(s) request being handled by a web server.
+ ///
+ public interface IHttpContext : IMimeTypeProvider
+ {
+ ///
+ /// Gets a unique identifier for a HTTP context.
+ ///
+ string Id { get; }
+
+ ///
+ /// Gets a used to stop processing of this context.
+ ///
+ CancellationToken CancellationToken { get; }
+
+ ///
+ /// Gets the server IP address and port number to which the request is directed.
+ ///
+ IPEndPoint LocalEndPoint { get; }
+
+ ///
+ /// Gets the client IP address and port number from which the request originated.
+ ///
+ IPEndPoint RemoteEndPoint { get; }
+
+ ///
+ /// Gets the HTTP request.
+ ///
+ IHttpRequest Request { get; }
+
+ ///
+ /// Gets the route matched by the requested URL path.
+ ///
+ RouteMatch Route { get; }
+
+ ///
+ /// Gets the requested path, relative to the innermost module's base path.
+ ///
+ ///
+ /// This property derives from the path specified in the requested URL, stripped of the
+ /// BaseRoute of the handling module.
+ /// This property is in itself a valid URL path, including an initial
+ /// slash (/) character.
+ ///
+ string RequestedPath { get; }
+
+ ///
+ /// Gets the HTTP response object.
+ ///
+ IHttpResponse Response { get; }
+
+ ///
+ /// Gets the user.
+ ///
+ IPrincipal User { get; }
+
+ ///
+ /// Gets the session proxy associated with this context.
+ ///
+ ISessionProxy Session { get; }
+
+ ///
+ /// Gets a value indicating whether compressed request bodies are supported.
+ ///
+ ///
+ bool SupportCompressedRequests { get; }
+
+ ///
+ /// Gets the dictionary of data to pass trough the EmbedIO pipeline.
+ ///
+ IDictionary