Got at least one data fetching method working; turns out, we can't use a patched LogicStack to get the data

This commit is contained in:
2026-01-14 22:11:11 +01:00
parent 40a8431464
commit 3f7122d30a
350 changed files with 41444 additions and 119 deletions

15
.idea/.idea.RemoteControl/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,15 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/modules.xml
/contentModel.xml
/projectSettingsUpdater.xml
/.idea.RemoteControl.iml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

7
.idea/.idea.RemoteControl/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

11
About/About.xml Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<ModMetadata xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>RemoteControl</Name>
<Author>TheQuux</Author>
<Version>1.0</Version>
<Description>Provides an API for controlling </Description>
<WorkshopHandle>0</WorkshopHandle>
<Tags>
<Tag>LaunchPad</Tag>
</Tags>
</ModMetadata>

BIN
About/Preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
About/Thumb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 KiB

View File

@@ -1,36 +0,0 @@
using HarmonyLib;
using System;
using UnityEngine;
namespace ExamplePatchMod
{
#region BepInEx
[BepInEx.BepInPlugin(pluginGuid, pluginName, pluginVersion)]
public class ExamplePatchMod : BepInEx.BaseUnityPlugin
{
public const string pluginGuid = "com.username.ExamplePatchMod";
public const string pluginName = "ExamplePatchMod";
public const string pluginVersion = "1.0";
public static void Log(string line)
{
Debug.Log("[" + pluginName + "]: " + line);
}
void Awake()
{
try
{
var harmony = new Harmony(pluginGuid);
harmony.PatchAll();
Log("Patch succeeded");
}
catch (Exception e)
{
Log("Patch Failed");
Log(e.ToString());
}
}
}
#endregion
}

View File

@@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{81D8C460-4627-489B-8D5E-A0640866290F}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>ExamplePatchMod</RootNamespace>
<AssemblyName>ExamplePatchMod</AssemblyName>
<LangVersion>9.0</LangVersion>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="0Harmony">
<HintPath>D:\SteamLibrary\steamapps\common\Stationeers\BepInEx\core\0Harmony.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>D:\SteamLibrary\steamapps\common\Stationeers\rocketstation_Data\Managed\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="BepInEx">
<HintPath>D:\SteamLibrary\steamapps\common\Stationeers\BepInEx\core\BepInEx.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="UnityEngine">
<HintPath>D:\SteamLibrary\steamapps\common\Stationeers\rocketstation_Data\Managed\UnityEngine.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>D:\SteamLibrary\steamapps\common\Stationeers\rocketstation_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="BepInEx.cs" />
<Compile Include="Patches\ExamplePatchClass.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -1,12 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ExamplePatchMod.Patches
{
internal class ExamplePatchClass
{
}
}

69
Patches/Tick.cs Normal file
View File

@@ -0,0 +1,69 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Assets.Scripts;
using Assets.Scripts.Objects.Electrical;
using HarmonyLib;
using JetBrains.Annotations;
namespace RemoteControl.Patches
{
[HarmonyPatch(typeof(LogicStack), nameof(LogicStack.LogicStackTick))]
[SuppressMessage("ReSharper", "InconsistentNaming")]
// ReSharper disable once InconsistentNaming
public class LogicStack_LogicStackTick
{
private struct State
{
internal object LockObj;
internal bool Taken;
}
[UsedImplicitly]
private static void Prefix(out State __state)
{
__state = new State
{
LockObj = SubscriptionManager.Lock,
Taken = false
};
if (!GameManager.RunSimulation) return;
// System.Threading.Monitor.Enter(__state.LockObj, ref __state.Taken);
try
{
RemoteControl.Log("logic stack tick: start prefix");
SubscriptionManager.ApplyUpdates();
RemoteControl.Log("logic stack tick: end prefix");
}
catch (Exception e)
{
RemoteControl.Log($"prefix: Exception {e}:\n {e.StackTrace}");
}
}
[UsedImplicitly]
private static void Postfix(State __state)
{
try
{
if (!GameManager.RunSimulation) return;
SubscriptionManager.RescanNetworks();
}
catch (Exception e)
{
RemoteControl.Log("logic stack tick: start postfix");
RemoteControl.Log($"postfix: Exception {e}: \n{e.StackTrace}");
RemoteControl.Log("logic stack tick: end postfix");
}
finally
{
// if (__state.Taken)
// {
// System.Threading.Monitor.Exit(__state.LockObj);
// }
}
}
}
}

View File

@@ -0,0 +1,14 @@
using Assets.Scripts;
using HarmonyLib;
namespace Remotecontrol.Patches
{
[HarmonyPatch(typeof(WorldManager), nameof(WorldManager.StartWorld))]
public class WorldManagerStartWorld
{
static void Prefix(WorldManager __instance)
{
}
}
}

View File

@@ -5,11 +5,11 @@ using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("ExamplePatchMod")]
[assembly: AssemblyTitle("RemoteControl")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ExamplePatchMod")]
[assembly: AssemblyProduct("RemoteControl")]
[assembly: AssemblyCopyright("Copyright © 2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

View File

@@ -15,9 +15,9 @@ Follow these steps to prepare your Stationeers mod project:
4. **Update Project References**
- In the **Solution Explorer**, press `CTRL + A` to select all files.
- Press `CTRL + F` to open the **Find and Replace** window.
- Find all instances of `ExamplePatchMod` and replace them with your mod name, e.g., `MyStationeersMod`.
- Find all instances of `RemoteControl` and replace them with your mod name, e.g., `MyStationeersMod`.
- Click **Replace All**.
5. **Update Project Properties**
- On the top menu, go to **Project → [YourModName] Properties**.
- Change both the **Assembly Name** and **Default Namespace** from `ExamplePatchMod` to your mod name.
- Change both the **Assembly Name** and **Default Namespace** from `RemoteControl` to your mod name.

410
RemoteControl.csproj Normal file
View File

@@ -0,0 +1,410 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{81D8C460-4627-489B-8D5E-A0640866290F}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>RemoteControl</RootNamespace>
<AssemblyName>RemoteControl</AssemblyName>
<LangVersion>9.0</LangVersion>
<TargetFrameworkVersion>v4.8.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="0Harmony">
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\Stationeers\BepInEx\core\0Harmony.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\Stationeers\rocketstation_Data\Managed\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="BepInEx">
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\Stationeers\BepInEx\core\BepInEx.dll</HintPath>
</Reference>
<Reference Include="System"/>
<Reference Include="System.Core"/>
<Reference Include="System.Xml.Linq"/>
<Reference Include="System.Data.DataSetExtensions"/>
<Reference Include="Microsoft.CSharp"/>
<Reference Include="System.Data"/>
<Reference Include="System.Net.Http"/>
<Reference Include="System.Xml"/>
<Reference Include="System.IO.Compression"/>
<Reference Include="System.Text" />
<Reference Include="System.Text.Json" />
<Reference Include="UniTask">
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\Stationeers\rocketstation_Data\Managed\UniTask.dll</HintPath>
</Reference>
<Reference Include="UnityEngine">
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\Stationeers\rocketstation_Data\Managed\UnityEngine.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\Stationeers\rocketstation_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
</Reference>
<Reference Include="netstandard" />
<Reference Include="System.IO.Compression.ZipFile" />
<Reference Include="System.Web"/>
</ItemGroup>
<ItemGroup>
<Compile Include="Patches\Tick.cs"/>
<Compile Include="Patches\WorldManagerStop.cs"/>
<Compile Include="Properties\AssemblyInfo.cs"/>
<Compile Include="Scripts\Remotecontrol.cs"/>
<Compile Include="Scripts\SubscriptionManager.cs"/>
<Compile Include="Scripts\SubscriptionModule.cs"/>
<Compile Include="Utils\Extensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Actions\ActionModule.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Actions\RedirectModule.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Authentication\Auth.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Authentication\BasicAuthenticationModule.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Authentication\BasicAuthenticationModuleBase.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Authentication\BasicAuthenticationModuleExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\CompressionMethod.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\CompressionMethodNames.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Cors\CorsModule.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\EmbedIOInternalErrorException.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\ExceptionHandler.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\ExceptionHandlerCallback.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\DirectoryLister.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\FileCache.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\FileCache.Section.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\FileModule.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\FileModuleExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\FileRequestHandler.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\FileRequestHandlerCallback.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\FileSystemProvider.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\IDirectoryLister.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\IFileProvider.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\Internal\Base64Utility.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\Internal\EntityTag.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\Internal\FileCacheItem.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\Internal\HtmlDirectoryLister.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\Internal\MappedResourceInfoExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\MappedResourceInfo.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\ResourceFileProvider.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Files\ZipFileProvider.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpContextExtensions-Items.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpContextExtensions-Redirect.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpContextExtensions-Requests.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpContextExtensions-RequestStream.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpContextExtensions-Responses.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpContextExtensions-ResponseStream.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpContextExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpException-Shortcuts.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpException.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpExceptionHandler.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpExceptionHandlerCallback.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpHeaderNames.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpListenerMode.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpNotAcceptableException.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpRangeNotSatisfiableException.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpRedirectException.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpRequestExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpResponseExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpStatusDescription.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\HttpVerbs.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\ICookieCollection.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\IHttpContext.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\IHttpContextHandler.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\IHttpContextImpl.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\IHttpException.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\IHttpListener.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\IHttpMessage.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\IHttpRequest.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\IHttpResponse.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\IMimeTypeCustomizer.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\IMimeTypeProvider.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Internal\BufferingResponseStream.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Internal\CompressionStream.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Internal\CompressionUtility.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Internal\DummyWebModuleContainer.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Internal\LockableNameValueCollection.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Internal\MimeTypeCustomizer.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Internal\RequestHandlerPassThroughException.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Internal\TimeKeeper.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Internal\UriUtility.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Internal\WebModuleCollection.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\IWebModule.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\IWebModuleContainer.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\IWebServer.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\MimeType.Associations.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\MimeType.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\MimeTypeCustomizerExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\ModuleGroup.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\CookieList.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\EndPointManager.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\HttpListener.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\EndPointListener.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\HeaderUtility.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\HttpConnection.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\HttpConnection.InputState.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\HttpConnection.LineState.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\HttpListenerContext.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\HttpListenerPrefixCollection.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\HttpListenerRequest.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\HttpListenerResponse.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\HttpListenerResponseHelper.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\ListenerPrefix.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\ListenerUri.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\NetExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\RequestStream.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\ResponseStream.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\StringExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\SystemCookieCollection.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\SystemHttpContext.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\SystemHttpListener.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\SystemHttpRequest.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\SystemHttpResponse.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Net\Internal\WebSocketHandshakeResponse.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\RequestDeserializer.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\RequestDeserializerCallback`1.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\RequestHandler.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\RequestHandlerCallback.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\ResponseSerializer.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\ResponseSerializerCallback.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\BaseRouteAttribute.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\Route.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\RouteAttribute.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\RouteHandlerCallback.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\RouteMatch.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\RouteMatcher.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\RouteResolutionResult.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\RouteResolverBase`1.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\RouteResolverCollectionBase`2.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\RouteVerbResolver.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\RouteVerbResolverCollection.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\RoutingModule.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\RoutingModuleBase.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\RoutingModuleExtensions-AddHandlerFromBaseOrTerminalRoute.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\RoutingModuleExtensions-AddHandlerFromRouteMatcher.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\RoutingModuleExtensions-AddHandlerFromTerminalRoute.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\RoutingModuleExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Routing\SyncRouteHandlerCallback.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Security\BanInfo.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Security\IIPBanningCriterion.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Security\Internal\IPBanningExecutor.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Security\IPBanningConfiguration.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Security\IPBanningModule.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Security\IPBanningModuleExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Security\IPBanningRegexCriterion.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Security\IPBanningRequestsCriterion.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Sessions\Internal\DummySessionProxy.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Sessions\ISession.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Sessions\ISessionManager.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Sessions\ISessionProxy.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Sessions\LocalSessionManager.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Sessions\LocalSessionManager.SessionImpl.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Sessions\Session.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Sessions\SessionExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Sessions\SessionProxy.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\ComponentCollectionExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\ComponentCollection`1.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\DisposableComponentCollection`1.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\HttpDate.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\IComponentCollection`1.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\IPParser.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\MimeTypeProviderStack.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\NameValueCollectionExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\QValueList.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\QValueListExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\StringExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\UniqueIdGenerator.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\UrlEncodedDataParser.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\UrlPath.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\Validate-MimeType.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\Validate-Paths.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\Validate-Rfc2616.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\Validate-Route.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\Utilities\Validate.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebApi\FormDataAttribute.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebApi\FormFieldAttribute.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebApi\IRequestDataAttribute`1.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebApi\IRequestDataAttribute`2.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebApi\JsonDataAttribute.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebApi\QueryDataAttribute.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebApi\QueryFieldAttribute.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebApi\WebApiController.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebApi\WebApiModule.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebApi\WebApiModuleBase.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebApi\WebApiModuleExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebModuleBase.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebModuleContainer.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebModuleContainerExtensions-Actions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebModuleContainerExtensions-Cors.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebModuleContainerExtensions-Files.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebModuleContainerExtensions-Routing.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebModuleContainerExtensions-Security.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebModuleContainerExtensions-WebApi.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebModuleContainerExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebModuleExtensions-ExceptionHandlers.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebModuleExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebServer-Constants.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebServer.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebServerBase`1.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebServerExtensions-ExceptionHandliers.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebServerExtensions-SessionManager.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebServerExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebServerOptions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebServerOptionsBase.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebServerOptionsBaseExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebServerOptionsExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebServerState.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebServerStateChangedEventArgs.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebServerStateChangedEventHandler.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\CloseStatusCode.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Internal\Fin.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Internal\FragmentBuffer.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Internal\Mask.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Internal\MessageEventArgs.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Internal\PayloadData.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Internal\Rsv.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Internal\StreamExtensions.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Internal\SystemWebSocket.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Internal\SystemWebSocketReceiveResult.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Internal\WebSocket.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Internal\WebSocketContext.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Internal\WebSocketFrame.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Internal\WebSocketFrameStream.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Internal\WebSocketReceiveResult.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Internal\WebSocketStream.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\IWebSocket.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\IWebSocketContext.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\IWebSocketReceiveResult.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\Opcode.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\WebSocketException.cs"/>
<Compile Include="Vendor\EmbedIO-3.5.2\WebSockets\WebSocketModule.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Collections\CollectionCacheRepository.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\CompositeHashCode.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Configuration\ConfiguredObject.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Configuration\SettingsProvider.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Cryptography\Hasher.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\DateTimeSpan.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Definitions.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Definitions.Types.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Diagnostics\Benchmark.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Diagnostics\BenchmarkUnit.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Diagnostics\HighResolutionTimer.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\EnumHelper.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Enums.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Extensions.ByteArrays.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Extensions.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Extensions.Dates.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Extensions.Dictionaries.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Extensions.Enumerable.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Extensions.Exceptions.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Extensions.Functional.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Extensions.IEnumerable.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Extensions.IPropertyProxy.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Extensions.Reflection.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Extensions.Strings.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Extensions.Tasks.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Extensions.ValueTypes.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Formatters\CsvReader.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Formatters\CsvWriter.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Formatters\HumanizeJson.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Formatters\Json.Converter.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Formatters\Json.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Formatters\Json.Deserializer.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Formatters\Json.Serializer.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Formatters\Json.SerializerOptions.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Formatters\JsonPropertyAttribute.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Formatters\JsonSerializerCase.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\FromString.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\InternalErrorException.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Logging\ConsoleLogger.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Logging\DebugLogger.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Logging\FileLogger.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Logging\ILogger.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Logging\Logger.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Logging\LogLevel.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Logging\LogMessageReceivedEventArgs.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Logging\TextLogger.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Mappers\CopyableAttribute.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Mappers\IObjectMap.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Mappers\ObjectMap.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Mappers\ObjectMapper.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Mappers\ObjectMapper.PropertyInfoComparer.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Net\Internal\IPAddressValue.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Net\IPAddressRange.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Net\IPAddressRangeExtensions.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Net\IPAddressUtility.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\ObjectComparer.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Paginator.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Parsers\ArgumentOptionAttribute.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Parsers\ArgumentParse.Validator.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Parsers\ArgumentParser.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Parsers\ArgumentParser.TypeResolver.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Parsers\ArgumentParserSettings.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Parsers\ExpressionParser.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Parsers\Operator.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Parsers\Token.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Parsers\Tokenizer.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Parsers\TokenType.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Parsers\VerbOptionAttribute.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Reflection\AttributeCache.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Reflection\ConstructorTypeCache.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Reflection\ExtendedTypeInfo.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Reflection\IPropertyProxy.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Reflection\MethodInfoCache.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Reflection\PropertyInfoProxy.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Reflection\PropertyTypeCache.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Reflection\TypeCache.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\SelfCheck.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\SingletonBase.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\StringConversionException.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\StructEndiannessAttribute.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\SwanRuntime.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Terminal.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Terminal.Graphics.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Terminal.Interaction.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Terminal.Output.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Terminal.Settings.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\TerminalWriters.Enums.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\AtomicBoolean.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\AtomicDateTime.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\AtomicDouble.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\AtomicEnum.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\AtomicInteger.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\AtomicLong.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\AtomicTimeSpan.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\AtomicTypeBase.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\CancellationTokenOwner.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\ExclusiveTimer.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\ISyncLocker.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\IWaitEvent.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\IWorker.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\IWorkerDelayProvider.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\PeriodicTask.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\RunnerBase.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\SyncLockerFactory.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\WaitEventFactory.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Threading\WorkerState.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Validators\IValidator.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Validators\ObjectValidationResult.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Validators\ObjectValidator.cs"/>
<Compile Include="Vendor\Swan.Lite-3.1.0\Validators\Validators.cs"/>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36511.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExamplePatchMod", "ExamplePatchMod.csproj", "{81D8C460-4627-489B-8D5E-A0640866290F}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RemoteControl", "RemoteControl.csproj", "{81D8C460-4627-489B-8D5E-A0640866290F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

246
Scripts/Remotecontrol.cs Normal file
View File

@@ -0,0 +1,246 @@
using System;
using UnityEngine;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Assets.Scripts;
using Assets.Scripts.Networks;
using Assets.Scripts.Objects.Electrical;
using Assets.Scripts.Objects.Motherboards;
using BepInEx;
using BepInEx.Configuration;
using Cysharp.Threading.Tasks;
using HarmonyLib;
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using RemoteControl.Message;
using RemoteControl.Utils;
using Swan;
using GameDevice = Assets.Scripts.Objects.Pipes.Device;
namespace RemoteControl
{
[BepInPlugin(pluginGuid, pluginName, pluginVersion)]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class RemoteControl : BepInEx.BaseUnityPlugin
{
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once MemberCanBePrivate.Global
public static ConfigEntry<int> Port;
// ReSharper disable once MemberCanBePrivate.Global
public static ConfigEntry<bool> ListenOnAllInterfaces;
private readonly Harmony _harmony = new Harmony(pluginGuid);
public WebServer WebServer { get; private set; }
private CancellationTokenSource _modLifecycle = new();
public const string pluginGuid = "com.thequux.stationeers.RemoteControl";
public const string pluginName = "RemoteControl";
public const string pluginVersion = "1.0";
public static void Log(string line)
{
Debug.Log("[" + pluginName + "]: " + line);
}
private void StartRC()
{
if (!_modLifecycle.IsCancellationRequested)
{
_modLifecycle.Cancel();
}
_modLifecycle = new CancellationTokenSource();
Port = Config.Bind(new ConfigDefinition("Server", "Port"), 39125,
new ConfigDescription("The port to listen on"));
ListenOnAllInterfaces = Config.Bind(
new ConfigDefinition("Server", "ListenOnAllInterfaces"),
false,
new ConfigDescription("If set, listen on all interfaces. Otherwise, listen only on localhost"));
var subscriptionModule = new SubscriptionModule("/subscribe");
WebServer = new WebServer(o =>
o.WithUrlPrefix($"http://{(ListenOnAllInterfaces.Value ? "0.0.0.0" : "localhost")}:{Port.Value}/")
.WithEmbedIOHttpListener()
)
.WithWebApi("/api/v1", m => m.WithController<ApiController>())
.WithModule(subscriptionModule);
WebServer.Start(_modLifecycle.Token);
_harmony.PatchAll();
foreach (var patchedMethod in _harmony.GetPatchedMethods())
{
Log($"Patched {patchedMethod.FullDescription()}");
}
Log($"Patched {_harmony.GetPatchedMethods().Count()} methods total");
}
public void OnLoaded(List<GameObject> prefabs)
{
// Start();
#if DEVELOPMENT_BUILD
Debug.Log($"Loaded {prefabs.Count} prefabs");
#endif
}
public void OnUnloaded(List<GameObject> prefabs)
{
// Stop();
}
public void StopRC()
{
_modLifecycle.Cancel();
_harmony.UnpatchSelf();
}
public void Awake()
{
Log("Starting RemoteControl");
StartRC();
}
private void OnDestroy()
{
Log("Tearing down RemoteControl");
StopRC();
}
}
internal class ApiController : WebApiController
{
private static readonly Dictionary<string, LogicType> LtByName = EnumCollections.LogicTypes.AsLookupDict(true);
private static readonly Dictionary<string, LogicSlotType> StByName = EnumCollections.LogicSlotTypes.AsLookupDict(true);
private static Device GetDeviceJson(GameDevice device) =>
new()
{
ReferenceId = device.ReferenceId,
Name = device.DisplayName,
NameHash = device.GetNameHash(),
PrefabHash = device.PrefabHash,
PrefabName = device.PrefabName,
LogicValues = EnumCollections.LogicTypes.Values.Where(ty => device.CanLogicRead(ty))
.ToDictionary(ty => ty, ty => device.GetLogicValue(ty)),
Slots = Enumerable.Range(0, device.TotalSlots)
.Select(slot => EnumCollections.LogicSlotTypes.Values.Where(sty => device.CanLogicRead(sty, slot))
.ToDictionary(ty => ty, sty=> device.GetLogicValue(sty, slot)))
.ToList()
};
private static Device? GetDeviceJson(long referenceId)
{
var dev = Referencable.Find<GameDevice>(referenceId);
return dev == null ? null : GetDeviceJson(dev);
}
[Route(HttpVerbs.Get, "/networks")]
public Task<IList<string>> ListNetworks() => Task.FromResult(SubscriptionManager.GetProbes());
[Route(HttpVerbs.Get, "/networks/{networkId}")]
public Task<IDictionary<long, Device>> ListDevices(string networkId)
{
// lock (SubscriptionManager.Lock)
using (new LogTimer("ListDevices"))
{
var networks = new HashSet<CableNetwork>();
GameDevice.AllDevices.ForEach(dev =>
{
if (dev is CableAnalyser analyser)
{
Debug.Log($"Found CA {analyser.DisplayName}: {analyser.ReferenceId}");
networks.Add(analyser.CableNetwork);
}
});
var devices = new Dictionary<long, Device>();
foreach (var network in networks)
{
foreach (var device in network.DeviceList)
{
RemoteControl.Log($"Found {device.PrefabName}: {device.DisplayName}({device.ReferenceId})");
if (devices.ContainsKey(device.ReferenceId)) continue;
devices.Add(device.ReferenceId, GetDeviceJson(device));
}
}
return Task.FromResult<IDictionary<long, Device>>(devices);
}
}
[Route(HttpVerbs.Post, "/networks/{networkId}/device/{deviceId}/logic/{varId}")]
public async Task SetLogic(string networkId, long deviceId, string varId)
{
LogicType ltype;
if (!LtByName.TryGetValue(varId.ToLowerInvariant(), out ltype))
{
throw HttpException.NotFound();
}
var value = await HttpContext.GetRequestDataAsync<double>();
SubscriptionManager.SetLogic(networkId, deviceId, ltype, value);
}
[Route(HttpVerbs.Post, "/networks/{networkId}/device/{deviceId}/slot/{slotId}/{varId}")]
public async Task SetSlot(string networkId, long deviceId, int slotId, string varId)
{
LogicSlotType ltype;
if (!StByName.TryGetValue(varId.ToLowerInvariant(), out ltype))
{
throw HttpException.NotFound();
}
var value = await HttpContext.GetRequestDataAsync<double>();
SubscriptionManager.SetSlot(networkId, deviceId, slotId, ltype, value);
}
[Route(HttpVerbs.Patch, "/networks/{networkId}/device/{deviceId}")]
public async Task PatchDevice(string networkId, long deviceId)
{
var values = await HttpContext.GetRequestDataAsync<Dictionary<string, double>>();
if (values.Keys.Any(key => !LtByName.ContainsKey(key.ToLowerInvariant())))
{
throw HttpException.BadRequest();
}
foreach (var keyValuePair in values)
{
SubscriptionManager.SetLogic(networkId, deviceId, LtByName[keyValuePair.Key.ToLowerInvariant()], keyValuePair.Value);
}
}
[Route(HttpVerbs.Get, "/networks/{networkId}/device/{deviceId}/code")]
public async UniTask<string> GetCode(string networkId, long deviceId)
{
await UniTask.SwitchToMainThread(HttpContext.CancellationToken);
string source;
// lock (SubscriptionManager.Lock)
{
var device = SubscriptionManager.GetDevice(networkId, deviceId) ?? throw HttpException.NotFound();
if (device is not ICircuitHolder)
{
throw HttpException.NotFound();
}
source = ((ICircuitHolder)device).GetSourceCode();
}
return source;
}
}
}

View File

@@ -0,0 +1,390 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Assets.Scripts;
using Assets.Scripts.Networks;
using Assets.Scripts.Objects.Electrical;
using Assets.Scripts.Objects.Motherboards;
using Assets.Scripts.Util;
using JetBrains.Annotations;
using RemoteControl.Message;
using Swan;
using UnityEngine;
using GameDevice = Assets.Scripts.Objects.Pipes.Device;
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable MemberCanBePrivate.Global
namespace RemoteControl
{
public class DataNetwork
{
public long ReferenceId { get; private set; }
internal readonly List<CableAnalyser> Probes = new ();
internal CableNetwork Network { get; private set; }
private readonly HashSet<long> _knownIds = new();
internal readonly Dictionary<long, Device> DeviceCache = new();
public DataNetwork(CableNetwork network)
{
ReferenceId = network.ReferenceId;
Network = network;
}
internal void RescanNetworkComposition()
{
_knownIds.Clear();
_knownIds.UnionWith(Network.DataDeviceList.Select(device => device.ReferenceId));
DeviceCache.Keys.Except(_knownIds).ToList().ForEach(device => DeviceCache.Remove(device));
_knownIds.Except(DeviceCache.Keys).ToList().ForEach(device =>
{
var newDev = new Device();
var dev = Referencable.Find<GameDevice>(device);
if (dev == null) return;
newDev.ReferenceId = device;
newDev.PrefabHash = dev.PrefabHash;
newDev.PrefabName = dev.PrefabName;
DeviceCache.Add(device, newDev);
});
UpdateAllDevices();
}
private void UpdateAllDevices()
{
foreach (var kvp in DeviceCache)
{
var device = kvp.Value;
var gameObj = Referencable.Find<GameDevice>(kvp.Key);
if (gameObj == null) continue;
device.LogicValues.Clear();
foreach (var type in EnumCollections.LogicTypes.Values)
{
if (gameObj.CanLogicRead(type))
{
device.LogicValues[type] = gameObj.GetLogicValue(type);
}
}
var slotCount = gameObj.TotalSlots;
while (device.Slots.Count > slotCount)
{
device.Slots.RemoveAt(device.Slots.Count - 1);
}
for (int slotIndex = 0; slotIndex < slotCount; slotIndex++)
{
if (device.Slots.Count <= slotIndex)
{
device.Slots.Add(new Dictionary<LogicSlotType, double>());
}
foreach (var type in EnumCollections.LogicSlotTypes.Values)
{
if (gameObj.CanLogicRead(type, slotIndex))
{
device.Slots[slotIndex][type] = gameObj.GetLogicValue(type, slotIndex);
}
}
}
}
}
public DataNetwork Reset(CableNetwork cnet)
{
ReferenceId = cnet.ReferenceId;
Network = cnet;
Probes.Clear();
DeviceCache.Clear();
_knownIds.Clear();
return this;
}
}
public readonly struct LogicUpdate : IEquatable<LogicUpdate>
{
public readonly long TargetReferenceId;
public readonly LogicType LogicType;
public LogicUpdate(long targetReferenceId, LogicType logicType)
{
TargetReferenceId = targetReferenceId;
LogicType = logicType;
}
public bool Equals(LogicUpdate other)
{
return TargetReferenceId == other.TargetReferenceId && LogicType == other.LogicType;
}
public override bool Equals(object obj)
{
return obj is LogicUpdate other && Equals(other);
}
public override int GetHashCode()
{
return CompositeHashCode.Using(TargetReferenceId, LogicType);
}
}
public readonly struct SlotUpdate : IEquatable<SlotUpdate>
{
public readonly long ReferenceId;
public readonly int Slot;
public readonly LogicSlotType Type;
public SlotUpdate(long referenceID, int slot, LogicSlotType type)
{
ReferenceId = referenceID;
Slot = slot;
Type = type;
}
public bool Equals(SlotUpdate other)
{
return ReferenceId == other.ReferenceId && Slot == other.Slot && Type == other.Type;
}
public override bool Equals(object obj)
{
return obj is SlotUpdate other && Equals(other);
}
public override int GetHashCode()
{
return CompositeHashCode.Using(ReferenceId, Slot, Type);
}
}
public static class SubscriptionManager
{
public static readonly object Lock = new();
private static readonly Dictionary<long, DataNetwork> DataNetworks = new();
// private Dictionary<string, List<RemoteControlLimpet>> _probesByName = new();
// private Dictionary<long, RemoteControlLimpet> _probesById = new();
private static readonly Dictionary<LogicUpdate, double> PendingUpdates = new();
private static readonly Dictionary<SlotUpdate, double> PendingSlotUpdates = new();
[CanBeNull]
public static Device FindCachedDevice(string probeName, long referenceID)
{
foreach (var analyzer in GetDataNetwork(probeName))
{
if (analyzer.DeviceCache.TryGetValue(referenceID, out var device))
{
return device;
}
}
return null;
}
// Can be called from any thread
public static bool SetLogic(string probeName, long referenceID, LogicType type, double value)
{
// lock (Lock)
{
var dev = FindCachedDevice(probeName, referenceID);
if (dev == null)
{
return false;
}
dev.LogicValues[type] = value;
PendingUpdates[new LogicUpdate(referenceID, type)] = value;
}
return true;
}
// Can be called from any thread
public static bool SetSlot(string probeName, long referenceID, int slot, LogicSlotType type, double value)
{
// lock (Lock)
{
var dev = FindCachedDevice(probeName, referenceID);
if (dev != null && dev.Slots.Count > slot && dev.Slots[slot].ContainsKey(type))
{
dev.Slots[slot][type] = value;
PendingSlotUpdates[new SlotUpdate(referenceID, slot, type)] = value;
return true;
}
else
{
return false;
}
}
}
public static DataNetwork GetDataNetwork(CableNetwork network)
{
if (DataNetworks.TryGetValue(network.ReferenceId, out var dataNetwork))
{
return dataNetwork;
}
else
{
var ret = new DataNetwork(network);
DataNetworks.Add(network.ReferenceId, ret);
return ret;
}
}
public static IEnumerable<DataNetwork> GetDataNetwork(string probeName)
{
// lock (Lock)
{
List<DataNetwork> networks = new();
GameDevice.AllDevices.ForEach(dev =>
{
if (dev is CableAnalyser analyzer)
{
networks.Add(GetDataNetwork(analyzer.CableNetwork));
}
});
return networks;
}
}
public static GameDevice GetDevice(long referenceID)
{
var device = Referencable.Find<GameDevice>(referenceID);
if (!DataNetworks.Values.Any((item) => item.Network.DataDeviceList.Contains(device)))
return null;
return device;
}
public static GameDevice GetDevice(string probeName, long referenceID)
{
var device = Referencable.Find<GameDevice>(referenceID);
if (GetDataNetwork(probeName).Any(network => network.DeviceCache.ContainsKey(referenceID)))
{
return device;
}
return null;
}
/// <summary>
/// Called from Unity thread pool before the logic tick
/// </summary>
/// <exception cref="NotImplementedException"></exception>
public static void ApplyUpdates()
{
lock (Lock)
{
foreach (var update in PendingUpdates)
{
var device = GetDevice(update.Key.TargetReferenceId);
if (!device.CanLogicWrite(update.Key.LogicType))
{
device.SetLogicValue(update.Key.LogicType, update.Value);
}
}
foreach (var update in PendingSlotUpdates)
{
var device = Referencable.Find<Assets.Scripts.Objects.Pipes.Device>(update.Key.ReferenceId);
if (!DataNetworks.Values.Any((item) => item.Network.DataDeviceList.Contains(device)))
continue;
if (!device.CanLogicWrite(update.Key.Type, update.Key.Slot))
{
device.SetLogicValue(update.Key.Type, update.Key.Slot, update.Value);
}
}
PendingUpdates.Clear();
PendingSlotUpdates.Clear();
}
}
public static void RescanNetworks()
{
using (new LogTimer("RescanNetworks"))
{
HashSet<CableAnalyser> scannedAnalyzers = new();
HashSet<CableNetwork> scannedNetworks = new();
scannedNetworks.Clear();
GameDevice.AllDevices.ForEach(dev =>
{
if (dev is CableAnalyser analyser)
{
scannedAnalyzers.Add(analyser);
scannedNetworks.Add(analyser.CableNetwork);
}
});
var removed =
new List<DataNetwork>(DataNetworks.Values.Where(dataNetwork =>
!scannedNetworks.Contains(dataNetwork.Network)));
foreach (var dataNet in removed)
{
DataNetworks.Remove(dataNet.ReferenceId);
}
foreach (var dataNet in DataNetworks.Values)
{
dataNet.Probes.Clear();
}
foreach (var analyzer in scannedAnalyzers)
{
if (DataNetworks.ContainsKey(analyzer.ReferenceId)) continue;
var dataNet = removed.Pop()?.Reset(analyzer.CableNetwork) ?? new DataNetwork(analyzer.CableNetwork);
dataNet.Probes.Add(analyzer);
}
// TODO: when we have our own device for tagging a network, make this triggered by on the DataNetworkChange method
foreach (var dataNet in DataNetworks.Values)
{
dataNet.RescanNetworkComposition();
}
}
}
public static IList<string> GetProbes()
{
List<string> probes = new();
GameDevice.AllDevices.ForEach(dev =>
{
if (dev is CableAnalyser)
{
probes.Add(dev.DisplayName);
}
});
return probes;
}
}
public class LogTimer : IDisposable
{
private readonly DateTime _startTime = DateTime.Now;
private string _action;
public LogTimer(string action)
{
RemoteControl.Log($"Beginning {action}");
_action = action;
}
public void Dispose()
{
var endTime = DateTime.Now;
var elapsed = endTime - _startTime;
Debug.Log($"Time taken for {_action}: " + elapsed);
}
}
}

View File

@@ -0,0 +1,74 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Assets.Scripts.Objects.Motherboards;
using EmbedIO.WebSockets;
using Swan;
namespace RemoteControl
{
internal class SubscriptionModule: WebSocketModule
{
private HashSet<IWebSocketContext> _jsonSubscribers = new();
public void SendUpdate(Message.Update update)
{
if (_jsonSubscribers.Count > 0)
{
var encoded = System.Text.Encoding.UTF8.GetBytes(update.ToJson());
foreach (var webSocketContext in _jsonSubscribers)
{
_ = SendAsync(webSocketContext, encoded);
}
}
}
public SubscriptionModule(string urlPath, bool enableConnectionWatchdog = true) : base(urlPath, enableConnectionWatchdog)
{
AddProtocols("json");
}
protected override Task OnMessageReceivedAsync(IWebSocketContext context, byte[] buffer, IWebSocketReceiveResult result)
{
return Task.CompletedTask;
}
protected override Task OnClientConnectedAsync(IWebSocketContext context)
{
_jsonSubscribers.Add(context);
return base.OnClientConnectedAsync(context);
}
protected override Task OnClientDisconnectedAsync(IWebSocketContext context)
{
_jsonSubscribers.Remove(context);
return base.OnClientDisconnectedAsync(context);
}
}
namespace Message
{
public class Update
{
public List<Network> Networks { get; set; } = new();
}
public class Network
{
public long ReferenceId { get; set; }
public List<string> Probes { get; set; } = new();
public List<Device> Devices { get; set; } = new();
}
public class Device
{
public long ReferenceId { get; set; }
public long NameHash{ get; set; }
public string Name{ get; set; }
public long PrefabHash{ get; set; }
public string PrefabName{ get; set; }
public Dictionary<LogicType, double> LogicValues { get; set; } = new();
public List<Dictionary<LogicSlotType, double>> Slots { get; set; }= new();
}
}
}

29
Utils/Extensions.cs Normal file
View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using Assets.Scripts;
using Swan;
namespace RemoteControl.Utils
{
internal static class Extensions
{
internal static Dictionary<string, TEnum> AsLookupDict<TEnum, TValue>(this EnumCollection<TEnum, TValue> collection, bool lowercase)
where TEnum : Enum, IConvertible, new()
where TValue : IConvertible, IEquatable<TValue>
{
var result = new Dictionary<string, TEnum>();
for (var i = 0; i < collection.Length; i++)
{
var name = collection.Names[i];
if (lowercase)
{
name = name.ToLowerInvariant();
}
var value = collection.Values[i];
result.Add(name, value);
result.Add(collection.ValuesAsInts[i].ToStringInvariant(), value);
}
return result;
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Threading.Tasks;
using EmbedIO.Utilities;
namespace EmbedIO.Actions
{
/// <summary>
/// A module that passes requests to a callback.
/// </summary>
/// <seealso cref="WebModuleBase" />
public class ActionModule : WebModuleBase
{
private readonly HttpVerbs _verb;
private readonly RequestHandlerCallback _handler;
/// <summary>
/// Initializes a new instance of the <see cref="ActionModule" /> class.
/// </summary>
/// <param name="baseRoute">The base route.</param>
/// <param name="verb">The HTTP verb that will be served by this module.</param>
/// <param name="handler">The callback used to handle requests.</param>
/// <exception cref="ArgumentNullException"><paramref name="handler"/> is <see langword="null"/>.</exception>
/// <seealso cref="WebModuleBase(string)"/>
public ActionModule(string baseRoute, HttpVerbs verb, RequestHandlerCallback handler)
: base(baseRoute)
{
_verb = verb;
_handler = Validate.NotNull(nameof(handler), handler);
}
/// <summary>
/// Initializes a new instance of the <see cref="ActionModule"/> class.
/// </summary>
/// <param name="handler">The handler.</param>
public ActionModule(RequestHandlerCallback handler)
: this("/", HttpVerbs.Any, handler)
{
}
/// <inheritdoc />
public override bool IsFinalHandler => false;
/// <inheritdoc />
protected override async Task OnRequestAsync(IHttpContext context)
{
if (_verb != HttpVerbs.Any && context.Request.HttpVerb != _verb)
return;
await _handler(context).ConfigureAwait(false);
context.SetHandled();
}
}
}

View File

@@ -0,0 +1,99 @@
using System;
using System.Net;
using System.Threading.Tasks;
using EmbedIO.Utilities;
namespace EmbedIO.Actions
{
/// <summary>
/// A module that redirects requests.
/// </summary>
/// <seealso cref="WebModuleBase" />
public class RedirectModule : WebModuleBase
{
private readonly Func<IHttpContext, bool>? _shouldRedirect;
/// <summary>
/// Initializes a new instance of the <see cref="RedirectModule"/> class
/// that will redirect all served requests.
/// </summary>
/// <param name="baseRoute">The base route.</param>
/// <param name="redirectUrl">The redirect URL.</param>
/// <param name="statusCode">The response status code; default is <c>302 - Found</c>.</param>
/// <exception cref="ArgumentNullException"><paramref name="redirectUrl"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para><paramref name="redirectUrl"/> is not a valid URL.</para>
/// <para>- or -</para>
/// <para><paramref name="statusCode"/> is not a redirection (3xx) status code.</para>
/// </exception>
/// <seealso cref="WebModuleBase(string)"/>
public RedirectModule(string baseRoute, string redirectUrl, HttpStatusCode statusCode = HttpStatusCode.Found)
: this(baseRoute, redirectUrl, null, statusCode, false)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RedirectModule"/> class
/// that will redirect all requests for which the <paramref name="shouldRedirect"/> callback
/// returns <see langword="true"/>.
/// </summary>
/// <param name="baseRoute">The base route.</param>
/// <param name="redirectUrl">The redirect URL.</param>
/// <param name="shouldRedirect">A callback function that returns <see langword="true"/>
/// if a request must be redirected.</param>
/// <param name="statusCode">The response status code; default is <c>302 - Found</c>.</param>
/// <exception cref="ArgumentNullException">
/// <para><paramref name="redirectUrl"/> is <see langword="null"/>.</para>
/// <para>- or -</para>
/// <para><paramref name="shouldRedirect"/> is <see langword="null"/>.</para>
/// </exception>
/// <exception cref="ArgumentException">
/// <para><paramref name="redirectUrl"/> is not a valid URL.</para>
/// <para>- or -</para>
/// <para><paramref name="statusCode"/> is not a redirection (3xx) status code.</para>
/// </exception>
/// <seealso cref="WebModuleBase(string)"/>
public RedirectModule(string baseRoute, string redirectUrl, Func<IHttpContext, bool>? shouldRedirect, HttpStatusCode statusCode = HttpStatusCode.Found)
: this(baseRoute, redirectUrl, shouldRedirect, statusCode, true)
{
}
private RedirectModule(string baseRoute, string redirectUrl, Func<IHttpContext, bool>? shouldRedirect, HttpStatusCode statusCode, bool useCallback)
: base(baseRoute)
{
RedirectUrl = Validate.Url(nameof(redirectUrl), redirectUrl);
var status = (int)statusCode;
if (status < 300 || status > 399)
throw new ArgumentException("Status code does not imply a redirection.", nameof(statusCode));
StatusCode = statusCode;
_shouldRedirect = useCallback ? Validate.NotNull(nameof(shouldRedirect), shouldRedirect) : null;
}
/// <inheritdoc />
public override bool IsFinalHandler => false;
/// <summary>
/// Gets the redirect URL.
/// </summary>
public string RedirectUrl { get; }
/// <summary>
/// Gets the response status code.
/// </summary>
public HttpStatusCode StatusCode { get; }
/// <inheritdoc />
protected override Task OnRequestAsync(IHttpContext context)
{
if (_shouldRedirect?.Invoke(context) ?? true)
{
context.Redirect(RedirectUrl, (int)StatusCode);
context.SetHandled();
}
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,31 @@
using System.Security.Principal;
namespace EmbedIO.Authentication
{
/// <summary>
/// Provides useful authentication-related constants.
/// </summary>
public static class Auth
{
/// <summary>
/// Gets an <see cref="IPrincipal"/> interface representing
/// no user. To be used instead of <see langword="null"/>
/// to initialize or set properties of type <see cref="IPrincipal"/>.
/// </summary>
public static IPrincipal NoUser { get; } = new GenericPrincipal(
new GenericIdentity(string.Empty, string.Empty),
null);
/// <summary>
/// Creates and returns an <see cref="IPrincipal"/> interface
/// representing an unauthenticated user, with the given
/// authentication type.
/// </summary>
/// <param name="authenticationType">The type of authentication used to identify the user.</param>
/// <returns>An <see cref="IPrincipal"/> interface.</returns>
public static IPrincipal CreateUnauthenticatedPrincipal(string authenticationType)
=> new GenericPrincipal(
new GenericIdentity(string.Empty, authenticationType),
null);
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace EmbedIO.Authentication
{
/// <summary>
/// Simple HTTP basic authentication module that stores credentials
/// in a <seealso cref="ConcurrentDictionary{TKey,TValue}"/>.
/// </summary>
public class BasicAuthenticationModule : BasicAuthenticationModuleBase
{
/// <summary>
/// Initializes a new instance of the <see cref="BasicAuthenticationModule"/> class.
/// </summary>
/// <param name="baseRoute">The base route.</param>
/// <param name="realm">The authentication realm.</param>
/// <remarks>
/// <para>If <paramref name="realm"/> is <see langword="null"/> or the empty string,
/// the <see cref="BasicAuthenticationModuleBase.Realm">Realm</see> property will be set equal to
/// <see cref="IWebModule.BaseRoute">BaseRoute</see>.</para>
/// </remarks>
public BasicAuthenticationModule(string baseRoute, string? realm = null)
: base(baseRoute, realm)
{
}
/// <summary>
/// Gets a dictionary of valid user names and passwords.
/// </summary>
/// <value>
/// The accounts.
/// </value>
public ConcurrentDictionary<string, string> Accounts { get; } = new ConcurrentDictionary<string, string>(StringComparer.InvariantCulture);
/// <inheritdoc />
protected override Task<bool> VerifyCredentialsAsync(string path, string userName, string password, CancellationToken cancellationToken)
=> Task.FromResult(VerifyCredentialsInternal(userName, password));
private bool VerifyCredentialsInternal(string userName, string password)
=> userName != null
&& Accounts.TryGetValue(userName, out var storedPassword)
&& string.Equals(password, storedPassword, StringComparison.Ordinal);
}
}

View File

@@ -0,0 +1,103 @@
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace EmbedIO.Authentication
{
/// <summary>
/// Implements <see href="https://tools.ietf.org/html/rfc7617">HTTP basic authentication</see>.
/// </summary>
public abstract class BasicAuthenticationModuleBase : WebModuleBase
{
private readonly string _wwwAuthenticateHeaderValue;
/// <summary>
/// Initializes a new instance of the <see cref="BasicAuthenticationModuleBase"/> class.
/// </summary>
/// <param name="baseRoute">The base URL path.</param>
/// <param name="realm">The authentication realm.</param>
/// <remarks>
/// <para>If <paramref name="realm"/> is <see langword="null"/> or the empty string,
/// the <see cref="Realm"/> property will be set equal to
/// <see cref="IWebModule.BaseRoute">BaseRoute</see>.</para>
/// </remarks>
protected BasicAuthenticationModuleBase(string baseRoute, string? realm)
: base(baseRoute)
{
Realm = string.IsNullOrEmpty(realm) ? BaseRoute : realm;
_wwwAuthenticateHeaderValue = $"Basic realm=\"{Realm}\" charset=UTF-8";
}
/// <inheritdoc />
public sealed override bool IsFinalHandler => false;
/// <summary>
/// Gets the authentication realm.
/// </summary>
public string Realm { get; }
/// <inheritdoc />
protected sealed override async Task OnRequestAsync(IHttpContext context)
{
async Task<bool> IsAuthenticatedAsync()
{
try
{
var (userName, password) = GetCredentials(context.Request);
return await VerifyCredentialsAsync(context.RequestedPath, userName, password, context.CancellationToken)
.ConfigureAwait(false);
}
catch (FormatException)
{
// Credentials were not formatted correctly.
return false;
}
}
context.Response.Headers.Set(HttpHeaderNames.WWWAuthenticate, _wwwAuthenticateHeaderValue);
if (!await IsAuthenticatedAsync().ConfigureAwait(false))
throw HttpException.Unauthorized();
}
/// <summary>
/// Verifies the credentials given in the <c>Authentication</c> request header.
/// </summary>
/// <param name="path">The URL path requested by the client. Note that this is relative
/// to the module's <see cref="WebModuleBase.BaseRoute">BaseRoute</see>.</param>
/// <param name="userName">The user name, or <see langword="null" /> if none has been given.</param>
/// <param name="password">The password, or <see langword="null" /> if none has been given.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> use to cancel the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> whose result will be <see langword="true" /> if the given credentials
/// are valid, <see langword="false" /> if they are not.</returns>
protected abstract Task<bool> VerifyCredentialsAsync(string path, string userName, string password, CancellationToken cancellationToken);
private static (string UserName, string Password) GetCredentials(IHttpRequest request)
{
var authHeader = request.Headers[HttpHeaderNames.Authorization];
if (authHeader == null)
return default;
if (!authHeader.StartsWith("basic ", StringComparison.OrdinalIgnoreCase))
return default;
string credentials;
try
{
credentials = WebServer.DefaultEncoding.GetString(Convert.FromBase64String(authHeader.Substring(6).Trim()));
}
catch (FormatException)
{
return default;
}
var separatorPos = credentials.IndexOf(':');
return separatorPos < 0
? (credentials, string.Empty)
: (credentials.Substring(0, separatorPos), credentials.Substring(separatorPos + 1));
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
namespace EmbedIO.Authentication
{
/// <summary>
/// Provides extension methods for <see cref="BasicAuthenticationModule"/>.
/// </summary>
public static class BasicAuthenticationModuleExtensions
{
/// <summary>
/// Adds a username and password to the <see cref="BasicAuthenticationModule.Accounts">Accounts</see> dictionary.
/// </summary>
/// <param name="this">The <see cref="BasicAuthenticationModule"/> on which this method is called.</param>
/// <param name="userName">The user name.</param>
/// <param name="password">The password.</param>
/// <returns><paramref name="this"/>, with the user name and password added.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="userName"/> is <see langword="null"/>.</exception>
/// <exception cref="OverflowException">
/// <para>The <see cref="BasicAuthenticationModule.Accounts">Accounts</see> dictionary already contains
/// the maximum number of elements (<see cref="int.MaxValue">MaxValue</see>).</para>
/// </exception>
/// <remarks>
/// <para>If a <paramref name="userName"/> account already exists,
/// its password is replaced with <paramref name="password"/>.</para>
/// </remarks>
public static BasicAuthenticationModule WithAccount(this BasicAuthenticationModule @this, string userName, string password)
{
@this.Accounts.AddOrUpdate(userName, password, (_, __) => password);
return @this;
}
}
}

View File

@@ -0,0 +1,29 @@
namespace EmbedIO
{
/// <summary>
/// Specifies the compression method used to compress a message on
/// the WebSocket connection.
/// </summary>
/// <remarks>
/// The compression methods that can be used are defined in
/// <see href="https://tools.ietf.org/html/rfc7692">
/// Compression Extensions for WebSocket</see>.
/// </remarks>
public enum CompressionMethod : byte
{
/// <summary>
/// Specifies no compression.
/// </summary>
None,
/// <summary>
/// Specifies "Deflate" compression.
/// </summary>
Deflate,
/// <summary>
/// Specifies GZip compression.
/// </summary>
Gzip,
}
}

View File

@@ -0,0 +1,27 @@
namespace EmbedIO
{
/// <summary>
/// Exposes constants for possible values of the <c>Content-Encoding</c> HTTP header.
/// </summary>
/// <see cref="CompressionMethod"/>
public static class CompressionMethodNames
{
/// <summary>
/// Specifies no compression.
/// </summary>
/// <see cref="CompressionMethod.None"/>
public const string None = "identity";
/// <summary>
/// Specifies the "Deflate" compression method.
/// </summary>
/// <see cref="CompressionMethod.Deflate"/>
public const string Deflate = "deflate";
/// <summary>
/// Specifies the GZip compression method.
/// </summary>
/// <see cref="CompressionMethod.Gzip"/>
public const string Gzip = "gzip";
}
}

130
Vendor/EmbedIO-3.5.2/Cors/CorsModule.cs vendored Normal file
View File

@@ -0,0 +1,130 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using EmbedIO.Utilities;
namespace EmbedIO.Cors
{
/// <summary>
/// Cross-origin resource sharing (CORS) control Module.
/// CORS is a mechanism that allows restricted resources (e.g. fonts)
/// on a web page to be requested from another domain outside the domain from which the resource originated.
/// </summary>
public class CorsModule : WebModuleBase
{
/// <summary>
/// A string meaning "All" in CORS headers.
/// </summary>
public const string All = "*";
private readonly string _origins;
private readonly string _headers;
private readonly string _methods;
private readonly string[] _validOrigins;
private readonly string[] _validMethods;
/// <summary>
/// Initializes a new instance of the <see cref="CorsModule" /> class.
/// </summary>
/// <param name="baseRoute">The base route.</param>
/// <param name="origins">The valid origins. The default is <see cref="All"/> (<c>*</c>).</param>
/// <param name="headers">The valid headers. The default is <see cref="All"/> (<c>*</c>).</param>
/// <param name="methods">The valid methods. The default is <see cref="All"/> (<c>*</c>).</param>
/// <exception cref="ArgumentNullException">
/// origins
/// or
/// headers
/// or
/// methods
/// </exception>
public CorsModule(
string baseRoute,
string origins = All,
string headers = All,
string methods = All)
: base(baseRoute)
{
_origins = origins ?? throw new ArgumentNullException(nameof(origins));
_headers = headers ?? throw new ArgumentNullException(nameof(headers));
_methods = methods ?? throw new ArgumentNullException(nameof(methods));
_validOrigins =
origins.ToLowerInvariant()
.SplitByComma(StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.ToArray();
_validMethods =
methods.ToLowerInvariant()
.SplitByComma(StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.ToArray();
}
/// <inheritdoc />
public override bool IsFinalHandler => false;
/// <inheritdoc />
protected override Task OnRequestAsync(IHttpContext context)
{
var isOptions = context.Request.HttpVerb == HttpVerbs.Options;
// If we allow all we don't need to filter
if (_origins == All && _headers == All && _methods == All)
{
context.Response.Headers.Set(HttpHeaderNames.AccessControlAllowOrigin, All);
if (isOptions)
{
ValidateHttpOptions(context);
context.SetHandled();
}
return Task.CompletedTask;
}
var currentOrigin = context.Request.Headers[HttpHeaderNames.Origin];
if (string.IsNullOrWhiteSpace(currentOrigin) && context.Request.IsLocal)
return Task.CompletedTask;
if (_origins == All)
return Task.CompletedTask;
if (_validOrigins.Contains(currentOrigin))
{
context.Response.Headers.Set(HttpHeaderNames.AccessControlAllowOrigin, currentOrigin);
if (isOptions)
{
ValidateHttpOptions(context);
context.SetHandled();
}
}
return Task.CompletedTask;
}
private void ValidateHttpOptions(IHttpContext context)
{
var requestHeadersHeader = context.Request.Headers[HttpHeaderNames.AccessControlRequestHeaders];
if (!string.IsNullOrWhiteSpace(requestHeadersHeader))
{
// TODO: Remove unwanted headers from request
context.Response.Headers.Set(HttpHeaderNames.AccessControlAllowHeaders, requestHeadersHeader);
}
var requestMethodHeader = context.Request.Headers[HttpHeaderNames.AccessControlRequestMethod];
if (string.IsNullOrWhiteSpace(requestMethodHeader))
return;
var currentMethods = requestMethodHeader.ToLowerInvariant()
.SplitByComma(StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim());
if (_methods != All && !currentMethods.Any(_validMethods.Contains))
throw HttpException.BadRequest();
context.Response.Headers.Set(HttpHeaderNames.AccessControlAllowMethods, requestMethodHeader);
}
}
}

View File

@@ -0,0 +1,64 @@
using System;
using System.Runtime.Serialization;
/*
* NOTE TO CONTRIBUTORS:
*
* Never use this exception directly.
* Use the methods in EmbedIO.Internal.SelfCheck instead.
*/
namespace EmbedIO
{
#pragma warning disable SA1642 // Constructor summary documentation should begin with standard text
/// <summary>
/// <para>The exception that is thrown by EmbedIO's internal diagnostic checks to signal a condition
/// most probably caused by an error in EmbedIO.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
[Serializable]
public class EmbedIOInternalErrorException : Exception
{
/// <summary>
/// <para>Initializes a new instance of the <see cref="EmbedIOInternalErrorException"/> class.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
public EmbedIOInternalErrorException()
{
}
/// <summary>
/// <para>Initializes a new instance of the <see cref="EmbedIOInternalErrorException"/> class.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
/// <param name="message">The message that describes the error.</param>
public EmbedIOInternalErrorException(string message)
: base(message)
{
}
/// <summary>
/// <para>Initializes a new instance of the <see cref="EmbedIOInternalErrorException"/> class.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception,
/// or <see langword="null"/> if no inner exception is specified.</param>
public EmbedIOInternalErrorException(string message, Exception? innerException)
: base(message, innerException)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="EmbedIOInternalErrorException"/> class.
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
/// <param name="info">The <see cref="SerializationInfo"></see> that holds the serialized object data about the exception being thrown.</param>
/// <param name="context">The <see cref="StreamingContext"></see> that contains contextual information about the source or destination.</param>
protected EmbedIOInternalErrorException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
#pragma warning restore SA1642
}

165
Vendor/EmbedIO-3.5.2/ExceptionHandler.cs vendored Normal file
View File

@@ -0,0 +1,165 @@
using System;
using System.Net;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using System.Web;
using System.Web.Util;
using Swan.Logging;
namespace EmbedIO
{
/// <summary>
/// Provides standard handlers for unhandled exceptions at both module and server level.
/// </summary>
/// <seealso cref="IWebServer.OnUnhandledException"/>
/// <seealso cref="IWebModule.OnUnhandledException"/>
public static class ExceptionHandler
{
/// <summary>
/// The name of the response header used by the <see cref="EmptyResponseWithHeaders" />
/// handler to transmit the type of the exception to the client.
/// </summary>
public const string ExceptionTypeHeaderName = "X-Exception-Type";
/// <summary>
/// The name of the response header used by the <see cref="EmptyResponseWithHeaders" />
/// handler to transmit the message of the exception to the client.
/// </summary>
public const string ExceptionMessageHeaderName = "X-Exception-Message";
/// <summary>
/// Gets or sets the contact information to include in exception responses.
/// </summary>
public static string? ContactInformation { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to include stack traces
/// in exception responses.
/// </summary>
public static bool IncludeStackTraces { get; set; }
/// <summary>
/// <para>Gets the default handler used by <see cref="WebServerBase{TOptions}"/>.</para>
/// <para>This is the same as <see cref="HtmlResponse"/>.</para>
/// </summary>
public static ExceptionHandlerCallback Default { get; } = HtmlResponse;
/// <summary>
/// Sends an empty <c>500 Internal Server Error</c> response.
/// </summary>
/// <param name="context">An <see cref="IHttpContext" /> interface representing the context of the request.</param>
/// <param name="exception">The unhandled exception.</param>
/// <returns>A <see cref="Task" /> representing the ongoing operation.</returns>
#pragma warning disable CA1801 // Unused parameter
public static Task EmptyResponse(IHttpContext context, Exception exception)
#pragma warning restore CA1801
{
context.Response.SetEmptyResponse((int)HttpStatusCode.InternalServerError);
return Task.CompletedTask;
}
/// <summary>
/// <para>Sends an empty <c>500 Internal Server Error</c> response,
/// with the following additional headers:</para>
/// <list type="table">
/// <listheader>
/// <term>Header</term>
/// <description>Value</description>
/// </listheader>
/// <item>
/// <term><c>X-Exception-Type</c></term>
/// <description>The name (without namespace) of the type of exception that was thrown.</description>
/// </item>
/// <item>
/// <term><c>X-Exception-Message</c></term>
/// <description>The <see cref="Exception.Message">Message</see> property of the exception.</description>
/// </item>
/// </list>
/// <para>The aforementioned header names are available as the <see cref="ExceptionTypeHeaderName" /> and
/// <see cref="ExceptionMessageHeaderName" /> properties, respectively.</para>
/// </summary>
/// <param name="context">An <see cref="IHttpContext" /> interface representing the context of the request.</param>
/// <param name="exception">The unhandled exception.</param>
/// <returns>A <see cref="Task" /> representing the ongoing operation.</returns>
public static Task EmptyResponseWithHeaders(IHttpContext context, Exception exception)
{
context.Response.SetEmptyResponse((int)HttpStatusCode.InternalServerError);
context.Response.Headers[ExceptionTypeHeaderName] = Uri.EscapeDataString(exception.GetType().Name);
context.Response.Headers[ExceptionMessageHeaderName] = Uri.EscapeDataString(exception.Message);
return Task.CompletedTask;
}
/// <summary>
/// Sends a <c>500 Internal Server Error</c> response with a HTML payload
/// briefly describing the error, including contact information and/or a stack trace
/// if specified via the <see cref="ContactInformation"/> and <see cref="IncludeStackTraces"/>
/// properties, respectively.
/// </summary>
/// <param name="context">An <see cref="IHttpContext" /> interface representing the context of the request.</param>
/// <param name="exception">The unhandled exception.</param>
/// <returns>A <see cref="Task" /> representing the ongoing operation.</returns>
public static Task HtmlResponse(IHttpContext context, Exception exception)
=> context.SendStandardHtmlAsync(
(int)HttpStatusCode.InternalServerError,
text =>
{
text.Write("<p>The server has encountered an error and was not able to process your request.</p>");
text.Write("<p>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.</p>");
text.Write("<p>The following information may help them in finding out what happened and restoring full functionality.</p>");
text.Write(
"<p><strong>Exception type:</strong> {0}<p><strong>Message:</strong> {1}",
WebUtility.HtmlEncode(exception.GetType().FullName ?? "<unknown>"),
WebUtility.HtmlEncode(exception.Message));
if (IncludeStackTraces)
{
text.Write(
"</p><p><strong>Stack trace:</strong></p><br><pre>{0}</pre>",
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.");
}
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Net;
using System.Threading.Tasks;
namespace EmbedIO
{
/// <summary>
/// A callback used to provide information about an unhandled exception occurred while processing a request.
/// </summary>
/// <param name="context">An <see cref="IHttpContext" /> interface representing the context of the request.</param>
/// <param name="exception">The unhandled exception.</param>
/// <returns>A <see cref="Task" /> representing the ongoing operation.</returns>
/// <remarks>
/// <para>When this delegate is called, the response's status code has already been set to
/// <see cref="HttpStatusCode.InternalServerError" />.</para>
/// <para>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 <c>throw HttpException.NotFound()</c> (or similar)
/// from a handler.</para>
/// </remarks>
public delegate Task ExceptionHandlerCallback(IHttpContext context, Exception exception);
}

View File

@@ -0,0 +1,20 @@
using EmbedIO.Files.Internal;
namespace EmbedIO.Files
{
/// <summary>
/// Provides standard directory listers for <see cref="FileModule"/>.
/// </summary>
/// <seealso cref="IDirectoryLister"/>
public static class DirectoryLister
{
/// <summary>
/// <para>Gets an <see cref="IDirectoryLister"/> interface
/// that produces a HTML listing of a directory.</para>
/// <para>The output of the returned directory lister
/// is the same as a directory listing obtained
/// by EmbedIO version 2.</para>
/// </summary>
public static IDirectoryLister Html => HtmlDirectoryLister.Instance;
}
}

View File

@@ -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<string, FileCacheItem> _items = new Dictionary<string, FileCacheItem>(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;
}
}
}
}

178
Vendor/EmbedIO-3.5.2/Files/FileCache.cs vendored Normal file
View File

@@ -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.
/// <summary>
/// A cache where one or more instances of <see cref="FileModule"/> can store hashes and file contents.
/// </summary>
public sealed partial class FileCache
#pragma warning restore CA1001
{
/// <summary>
/// The default value for the <see cref="MaxSizeKb"/> property.
/// </summary>
public const int DefaultMaxSizeKb = 10240;
/// <summary>
/// The default value for the <see cref="MaxFileSizeKb"/> property.
/// </summary>
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<string, Section> _sections = new ConcurrentDictionary<string, Section>(StringComparer.Ordinal);
private int _sectionCount; // Because ConcurrentDictionary<,>.Count is locking.
private int _maxSizeKb = DefaultMaxSizeKb;
private int _maxFileSizeKb = DefaultMaxFileSizeKb;
private PeriodicTask? _cleaner;
/// <summary>
/// Gets the default <see cref="FileCache"/> instance used by <see cref="FileModule"/>.
/// </summary>
public static FileCache Default
{
get
{
if (_defaultInstance != null)
return _defaultInstance;
lock (DefaultSyncRoot)
{
if (_defaultInstance == null)
_defaultInstance = new FileCache();
}
return _defaultInstance;
}
}
/// <summary>
/// <para>Gets or sets the maximum total size of cached data in kilobytes (1 kilobyte = 1024 bytes).</para>
/// <para>The default value for this property is stored in the <see cref="DefaultMaxSizeKb"/> constant field.</para>
/// <para>Setting this property to a value less lower han 1 has the same effect as setting it to 1.</para>
/// </summary>
public int MaxSizeKb
{
get => _maxSizeKb;
set => _maxSizeKb = Math.Max(value, 1);
}
/// <summary>
/// <para>Gets or sets the maximum size of a single cached file in kilobytes (1 kilobyte = 1024 bytes).</para>
/// <para>A single file's contents may be present in a cache more than once, if the file
/// is requested with different <c>Accept-Encoding</c> request headers. This property acts as a threshold
/// for the uncompressed size of a file.</para>
/// <para>The default value for this property is stored in the <see cref="DefaultMaxFileSizeKb"/> constant field.</para>
/// <para>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.</para>
/// <para>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.</para>
/// </summary>
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<string, Section>).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;
}
}
}

635
Vendor/EmbedIO-3.5.2/Files/FileModule.cs vendored Normal file
View File

@@ -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
{
/// <summary>
/// A module serving files and directory listings from an <see cref="IFileProvider"/>.
/// </summary>
/// <seealso cref="WebModuleBase" />
public class FileModule : WebModuleBase, IDisposable, IMimeTypeCustomizer
{
/// <summary>
/// <para>Default value for <see cref="DefaultDocument"/>.</para>
/// </summary>
public const string DefaultDocumentName = "index.html";
private readonly string _cacheSectionName = UniqueIdGenerator.GetNext();
private readonly MimeTypeCustomizer _mimeTypeCustomizer = new MimeTypeCustomizer();
private readonly ConcurrentDictionary<string, MappedResourceInfo>? _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;
/// <summary>
/// Initializes a new instance of the <see cref="FileModule"/> class,
/// using the specified cache.
/// </summary>
/// <param name="baseRoute">The base route.</param>
/// <param name="provider">An <see cref="IFileProvider"/> interface that provides access
/// to actual files and directories.</param>
/// <exception cref="ArgumentNullException"><paramref name="provider"/> is <see langword="null"/>.</exception>
public FileModule(string baseRoute, IFileProvider provider)
: base(baseRoute)
{
Provider = Validate.NotNull(nameof(provider), provider);
_mappingCache = Provider.IsImmutable
? new ConcurrentDictionary<string, MappedResourceInfo>()
: null;
}
/// <summary>
/// Finalizes an instance of the <see cref="FileModule"/> class.
/// </summary>
~FileModule()
{
Dispose(false);
}
/// <inheritdoc />
public override bool IsFinalHandler => true;
/// <summary>
/// Gets the <see cref="IFileProvider"/>interface that provides access
/// to actual files and directories served by this module.
/// </summary>
public IFileProvider Provider { get; }
/// <summary>
/// Gets or sets the <see cref="FileCache"/> used by this module to store hashes and,
/// optionally, file contents and rendered directory listings.
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
/// <exception cref="ArgumentNullException">This property is being set to <see langword="null"/>.</exception>
public FileCache Cache
{
get => _cache;
set
{
EnsureConfigurationNotLocked();
_cache = Validate.NotNull(nameof(value), value);
}
}
/// <summary>
/// <para>Gets or sets a value indicating whether this module caches the contents of files
/// and directory listings.</para>
/// <para>Note that the actual representations of files are stored in <see cref="FileCache"/>;
/// thus, for example, if a file is always requested with an <c>Accept-Encoding</c> of <c>gzip</c>,
/// only the gzipped contents of the file will be cached.</para>
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
public bool ContentCaching
{
get => _contentCaching;
set
{
EnsureConfigurationNotLocked();
_contentCaching = value;
}
}
/// <summary>
/// <para>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.</para>
/// <para>The default value for this property is the <see cref="DefaultDocumentName"/> constant.</para>
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
public string? DefaultDocument
{
get => _defaultDocument;
set
{
EnsureConfigurationNotLocked();
_defaultDocument = string.IsNullOrEmpty(value) ? null : value;
}
}
/// <summary>
/// <para>Gets or sets the default extension appended to requested URL paths that do not map
/// to any file or directory. Defaults to <see langword="null"/>.</para>
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
/// <exception cref="ArgumentException">This property is being set to a non-<see langword="null"/>,
/// non-empty string that does not start with a period (<c>.</c>).</exception>
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;
}
}
}
/// <summary>
/// <para>Gets or sets the <see cref="IDirectoryLister"/> interface used to generate
/// directory listing in this module.</para>
/// <para>A value of <see langword="null"/> (the default) disables the generation
/// of directory listings.</para>
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
public IDirectoryLister? DirectoryLister
{
get => _directoryLister;
set
{
EnsureConfigurationNotLocked();
_directoryLister = value;
}
}
/// <summary>
/// <para>Gets or sets a <see cref="FileRequestHandlerCallback"/> that is called whenever
/// the requested URL path could not be mapped to any file or directory.</para>
/// <para>The default is <see cref="FileRequestHandler.ThrowNotFound"/>.</para>
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
/// <exception cref="ArgumentNullException">This property is being set to <see langword="null"/>.</exception>
/// <seealso cref="FileRequestHandler"/>
public FileRequestHandlerCallback OnMappingFailed
{
get => _onMappingFailed;
set
{
EnsureConfigurationNotLocked();
_onMappingFailed = Validate.NotNull(nameof(value), value);
}
}
/// <summary>
/// <para>Gets or sets a <see cref="FileRequestHandlerCallback"/> that is called whenever
/// the requested URL path has been mapped to a directory, but directory listing has been
/// disabled by setting <see cref="DirectoryLister"/> to <see langword="null"/>.</para>
/// <para>The default is <see cref="FileRequestHandler.ThrowUnauthorized"/>.</para>
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
/// <exception cref="ArgumentNullException">This property is being set to <see langword="null"/>.</exception>
/// <seealso cref="FileRequestHandler"/>
public FileRequestHandlerCallback OnDirectoryNotListable
{
get => _onDirectoryNotListable;
set
{
EnsureConfigurationNotLocked();
_onDirectoryNotListable = Validate.NotNull(nameof(value), value);
}
}
/// <summary>
/// <para>Gets or sets a <see cref="FileRequestHandlerCallback"/> that is called whenever
/// the requested URL path has been mapped to a file or directory, but the request's
/// HTTP method is neither <c>GET</c> nor <c>HEAD</c>.</para>
/// <para>The default is <see cref="FileRequestHandler.ThrowMethodNotAllowed"/>.</para>
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
/// <exception cref="ArgumentNullException">This property is being set to <see langword="null"/>.</exception>
/// <seealso cref="FileRequestHandler"/>
public FileRequestHandlerCallback OnMethodNotAllowed
{
get => _onMethodNotAllowed;
set
{
EnsureConfigurationNotLocked();
_onMethodNotAllowed = Validate.NotNull(nameof(value), value);
}
}
/// <inheritdoc />
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);
/// <inheritdoc />
public void AddCustomMimeType(string extension, string mimeType)
=> _mimeTypeCustomizer.AddCustomMimeType(extension, mimeType);
/// <inheritdoc />
public void PreferCompression(string mimeType, bool preferCompression)
=> _mimeTypeCustomizer.PreferCompression(mimeType, preferCompression);
/// <summary>
/// Clears the part of <see cref="Cache"/> used by this module.
/// </summary>
public void ClearCache()
{
_mappingCache?.Clear();
_cacheSection?.Clear();
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources;
/// <see langword="false"/> to release only unmanaged resources.</param>
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);
}
/// <inheritdoc />
protected override void OnBeforeLockConfiguration()
{
base.OnBeforeLockConfiguration();
_mimeTypeCustomizer.Lock();
}
/// <inheritdoc />
protected override void OnStart(CancellationToken cancellationToken)
{
base.OnStart(cancellationToken);
_cacheSection = Cache.AddSection(_cacheSectionName);
Provider.ResourceChanged += _cacheSection.Remove;
Provider.Start(cancellationToken);
}
/// <inheritdoc />
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<IHttpResponse> 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);
}
}
}

View File

@@ -0,0 +1,282 @@
using System;
namespace EmbedIO.Files
{
/// <summary>
/// Provides extension methods for <see cref="FileModule"/> and derived classes.
/// </summary>
public static class FileModuleExtensions
{
/// <summary>
/// Sets the <see cref="FileCache"/> used by a module to store hashes and,
/// optionally, file contents and rendered directory listings.
/// </summary>
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
/// <param name="this">The module on which this method is called.</param>
/// <param name="value">An instance of <see cref="FileCache"/>.</param>
/// <returns><paramref name="this"/> with its <see cref="FileModule.Cache">Cache</see> property
/// set to <paramref name="value"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
/// <seealso cref="FileModule.Cache"/>
public static TModule WithCache<TModule>(this TModule @this, FileCache value)
where TModule : FileModule
{
@this.Cache = value;
return @this;
}
/// <summary>
/// Sets a value indicating whether a module caches the contents of files
/// and directory listings.
/// </summary>
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
/// <param name="this">The module on which this method is called.</param>
/// <param name="value"><see langword="true"/> to enable caching of contents;
/// <see langword="false"/> to disable it.</param>
/// <returns><paramref name="this"/> with its <see cref="FileModule.ContentCaching">ContentCaching</see> property
/// set to <paramref name="value"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
/// <seealso cref="FileModule.ContentCaching"/>
public static TModule WithContentCaching<TModule>(this TModule @this, bool value)
where TModule : FileModule
{
@this.ContentCaching = value;
return @this;
}
/// <summary>
/// Enables caching of file contents and directory listings on a module.
/// </summary>
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
/// <param name="this">The module on which this method is called.</param>
/// <returns><paramref name="this"/> with its <see cref="FileModule.ContentCaching">ContentCaching</see> property
/// set to <see langword="true"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
/// <seealso cref="FileModule.ContentCaching"/>
public static TModule WithContentCaching<TModule>(this TModule @this)
where TModule : FileModule
{
@this.ContentCaching = true;
return @this;
}
/// <summary>
/// Enables caching of file contents and directory listings on a module.
/// </summary>
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
/// <param name="this">The module on which this method is called.</param>
/// <param name="maxFileSizeKb"><see langword="true"/> sets the maximum size of a single cached file in kilobytes</param>
/// <param name="maxSizeKb"><see langword="true"/> sets the maximum total size of cached data in kilobytes</param>
/// <returns><paramref name="this"/> with its <see cref="FileModule.ContentCaching">ContentCaching</see> property
/// set to <see langword="true"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
/// <seealso cref="FileModule.ContentCaching"/>
public static TModule WithContentCaching<TModule>(this TModule @this, int maxFileSizeKb, int maxSizeKb)
where TModule : FileModule
{
@this.ContentCaching = true;
@this.Cache.MaxFileSizeKb = maxFileSizeKb;
@this.Cache.MaxSizeKb = maxSizeKb;
return @this;
}
/// <summary>
/// Disables caching of file contents and directory listings on a module.
/// </summary>
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
/// <param name="this">The module on which this method is called.</param>
/// <returns><paramref name="this"/> with its <see cref="FileModule.ContentCaching">ContentCaching</see> property
/// set to <see langword="false"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
/// <seealso cref="FileModule.ContentCaching"/>
public static TModule WithoutContentCaching<TModule>(this TModule @this)
where TModule : FileModule
{
@this.ContentCaching = false;
return @this;
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
/// <param name="this">The module on which this method is called.</param>
/// <param name="value">The name of the default document.</param>
/// <returns><paramref name="this"/> with its <see cref="FileModule.DefaultDocument">DefaultDocument</see> property
/// set to <paramref name="value"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
/// <seealso cref="FileModule.DefaultDocument"/>
public static TModule WithDefaultDocument<TModule>(this TModule @this, string value)
where TModule : FileModule
{
@this.DefaultDocument = value;
return @this;
}
/// <summary>
/// Sets the name of the default document to <see langword="null"/>.
/// </summary>
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
/// <param name="this">The module on which this method is called.</param>
/// <returns><paramref name="this"/> with its <see cref="FileModule.DefaultDocument">DefaultDocument</see> property
/// set to <see langword="null"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
/// <seealso cref="FileModule.DefaultDocument"/>
public static TModule WithoutDefaultDocument<TModule>(this TModule @this)
where TModule : FileModule
{
@this.DefaultDocument = null;
return @this;
}
/// <summary>
/// Sets the default extension appended to requested URL paths that do not map
/// to any file or directory.
/// </summary>
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
/// <param name="this">The module on which this method is called.</param>
/// <param name="value">The default extension.</param>
/// <returns><paramref name="this"/> with its <see cref="FileModule.DefaultExtension">DefaultExtension</see> property
/// set to <paramref name="value"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
/// <exception cref="ArgumentException"><paramref name="value"/> is a non-<see langword="null"/>,
/// non-empty string that does not start with a period (<c>.</c>).</exception>
/// <seealso cref="FileModule.DefaultExtension"/>
public static TModule WithDefaultExtension<TModule>(this TModule @this, string value)
where TModule : FileModule
{
@this.DefaultExtension = value;
return @this;
}
/// <summary>
/// Sets the default extension to <see langword="null"/>.
/// </summary>
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
/// <param name="this">The module on which this method is called.</param>
/// <returns><paramref name="this"/> with its <see cref="FileModule.DefaultExtension">DefaultExtension</see> property
/// set to <see langword="null"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
/// <seealso cref="FileModule.DefaultExtension"/>
public static TModule WithoutDefaultExtension<TModule>(this TModule @this)
where TModule : FileModule
{
@this.DefaultExtension = null;
return @this;
}
/// <summary>
/// Sets the <see cref="IDirectoryLister"/> interface used to generate
/// directory listing in a module.
/// </summary>
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
/// <param name="this">The module on which this method is called.</param>
/// <param name="value">An <see cref="IDirectoryLister"/> interface, or <see langword="null"/>
/// to disable the generation of directory listings.</param>
/// <returns><paramref name="this"/> with its <see cref="FileModule.DirectoryLister">DirectoryLister</see> property
/// set to <paramref name="value"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
/// <seealso cref="FileModule.DirectoryLister"/>
public static TModule WithDirectoryLister<TModule>(this TModule @this, IDirectoryLister value)
where TModule : FileModule
{
@this.DirectoryLister = value;
return @this;
}
/// <summary>
/// Sets a module's <see cref="FileModule.DirectoryLister">DirectoryLister</see> property
/// to <see langword="null"/>, disabling the generation of directory listings.
/// </summary>
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
/// <param name="this">The module on which this method is called.</param>
/// <returns><paramref name="this"/> with its <see cref="FileModule.DirectoryLister">DirectoryLister</see> property
/// set to <see langword="null"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
/// <seealso cref="FileModule.DirectoryLister"/>
public static TModule WithoutDirectoryLister<TModule>(this TModule @this)
where TModule : FileModule
{
@this.DirectoryLister = null;
return @this;
}
/// <summary>
/// Sets a <see cref="FileRequestHandlerCallback"/> that is called by a module whenever
/// the requested URL path could not be mapped to any file or directory.
/// </summary>
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
/// <param name="this">The module on which this method is called.</param>
/// <param name="callback">The method to call.</param>
/// <returns><paramref name="this"/> with its <see cref="FileModule.OnMappingFailed">OnMappingFailed</see> property
/// set to <paramref name="callback"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
/// <exception cref="ArgumentNullException"><paramref name="callback"/> is <see langword="null"/>.</exception>
/// <seealso cref="FileModule.OnMappingFailed"/>
/// <seealso cref="FileRequestHandler"/>
public static TModule HandleMappingFailed<TModule>(this TModule @this, FileRequestHandlerCallback callback)
where TModule : FileModule
{
@this.OnMappingFailed = callback;
return @this;
}
/// <summary>
/// Sets a <see cref="FileRequestHandlerCallback"/> that is called by a module whenever
/// the requested URL path has been mapped to a directory, but directory listing has been
/// disabled.
/// </summary>
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
/// <param name="this">The module on which this method is called.</param>
/// <param name="callback">The method to call.</param>
/// <returns><paramref name="this"/> with its <see cref="FileModule.OnDirectoryNotListable">OnDirectoryNotListable</see> property
/// set to <paramref name="callback"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
/// <exception cref="ArgumentNullException"><paramref name="callback"/> is <see langword="null"/>.</exception>
/// <seealso cref="FileModule.OnDirectoryNotListable"/>
/// <seealso cref="FileRequestHandler"/>
public static TModule HandleDirectoryNotListable<TModule>(this TModule @this, FileRequestHandlerCallback callback)
where TModule : FileModule
{
@this.OnDirectoryNotListable = callback;
return @this;
}
/// <summary>
/// Sets a <see cref="FileRequestHandlerCallback"/> 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 <c>GET</c> nor <c>HEAD</c>.
/// </summary>
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
/// <param name="this">The module on which this method is called.</param>
/// <param name="callback">The method to call.</param>
/// <returns><paramref name="this"/> with its <see cref="FileModule.OnMethodNotAllowed">OnMethodNotAllowed</see> property
/// set to <paramref name="callback"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
/// <exception cref="ArgumentNullException"><paramref name="callback"/> is <see langword="null"/>.</exception>
/// <seealso cref="FileModule.OnMethodNotAllowed"/>
/// <seealso cref="FileRequestHandler"/>
public static TModule HandleMethodNotAllowed<TModule>(this TModule @this, FileRequestHandlerCallback callback)
where TModule : FileModule
{
@this.OnMethodNotAllowed = callback;
return @this;
}
}
}

View File

@@ -0,0 +1,53 @@
using System.Threading.Tasks;
namespace EmbedIO.Files
{
/// <summary>
/// Provides standard handler callbacks for <see cref="FileModule"/>.
/// </summary>
/// <seealso cref="FileRequestHandlerCallback"/>
public static class FileRequestHandler
{
#pragma warning disable CA1801 // Unused parameters - Must respect FileRequestHandlerCallback signature.
/// <summary>
/// <para>Unconditionally passes a request down the module chain.</para>
/// </summary>
/// <param name="context">An <see cref="IHttpContext"/> interface representing the context of the request.</param>
/// <param name="info">If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping;
/// otherwise, <see langword="null"/>.</param>
/// <returns>This method never returns; it throws an exception instead.</returns>
public static Task PassThrough(IHttpContext context, MappedResourceInfo? info)
=> throw RequestHandler.PassThrough();
/// <summary>
/// <para>Unconditionally sends a <c>403 Unauthorized</c> response.</para>
/// </summary>
/// <param name="context">An <see cref="IHttpContext"/> interface representing the context of the request.</param>
/// <param name="info">If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping;
/// otherwise, <see langword="null"/>.</param>
/// <returns>This method never returns; it throws a <see cref="HttpException"/> instead.</returns>
public static Task ThrowUnauthorized(IHttpContext context, MappedResourceInfo? info)
=> throw HttpException.Unauthorized();
/// <summary>
/// <para>Unconditionally sends a <c>404 Not Found</c> response.</para>
/// </summary>
/// <param name="context">An <see cref="IHttpContext"/> interface representing the context of the request.</param>
/// <param name="info">If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping;
/// otherwise, <see langword="null"/>.</param>
/// <returns>This method never returns; it throws a <see cref="HttpException"/> instead.</returns>
public static Task ThrowNotFound(IHttpContext context, MappedResourceInfo? info)
=> throw HttpException.NotFound();
/// <summary>
/// <para>Unconditionally sends a <c>405 Method Not Allowed</c> response.</para>
/// </summary>
/// <param name="context">An <see cref="IHttpContext"/> interface representing the context of the request.</param>
/// <param name="info">If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping;
/// otherwise, <see langword="null"/>.</param>
/// <returns>This method never returns; it throws a <see cref="HttpException"/> instead.</returns>
public static Task ThrowMethodNotAllowed(IHttpContext context, MappedResourceInfo? info)
=> throw HttpException.MethodNotAllowed();
#pragma warning restore CA1801
}
}

View File

@@ -0,0 +1,13 @@
using System.Threading.Tasks;
namespace EmbedIO.Files
{
/// <summary>
/// A callback used to handle a request in <see cref="FileModule"/>.
/// </summary>
/// <param name="context">An <see cref="IHttpContext"/> interface representing the context of the request.</param>
/// <param name="info">If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping;
/// otherwise, <see langword="null"/>.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
public delegate Task FileRequestHandlerCallback(IHttpContext context, MappedResourceInfo? info);
}

View File

@@ -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
{
/// <summary>
/// Provides access to the local file system to a <see cref="FileModule"/>.
/// </summary>
/// <seealso cref="IFileProvider" />
public class FileSystemProvider : IDisposable, IFileProvider
{
private readonly FileSystemWatcher? _watcher;
/// <summary>
/// Initializes a new instance of the <see cref="FileSystemProvider"/> class.
/// </summary>
/// <remarks>
/// OSX doesn't support <see cref="FileSystemWatcher" />, the parameter <paramref name="isImmutable" /> will be always <see langword="true"/>.
/// </remarks>
/// <param name="fileSystemPath">The file system path.</param>
/// <param name="isImmutable"><see langword="true"/> if files and directories in
/// <paramref name="fileSystemPath"/> are not expected to change during a web server's
/// lifetime; <see langword="false"/> otherwise.</param>
/// <exception cref="ArgumentNullException"><paramref name="fileSystemPath"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="fileSystemPath"/> is not a valid local path.</exception>
/// <seealso cref="Validate.LocalPath"/>
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;
}
}
/// <summary>
/// Finalizes an instance of the <see cref="FileSystemProvider"/> class.
/// </summary>
~FileSystemProvider()
{
Dispose(false);
}
/// <inheritdoc />
public event Action<string>? ResourceChanged;
/// <summary>
/// Gets the file system path from which files are retrieved.
/// </summary>
public string FileSystemPath { get; }
/// <inheritdoc />
public bool IsImmutable { get; }
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public void Start(CancellationToken cancellationToken)
{
if (_watcher != null)
{
_watcher.Changed += Watcher_ChangedOrDeleted;
_watcher.Deleted += Watcher_ChangedOrDeleted;
_watcher.Renamed += Watcher_Renamed;
_watcher.EnableRaisingEvents = true;
}
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public Stream OpenFile(string path) => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
/// <inheritdoc />
public IEnumerable<MappedResourceInfo> GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider)
=> new DirectoryInfo(path).EnumerateFileSystemInfos()
.Select(fsi => GetMappedResourceInfo(mimeTypeProvider, fsi));
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources;
/// <see langword="false"/> to release only unmanaged resources.</param>
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);
}
}

View File

@@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace EmbedIO.Files
{
/// <summary>
/// Represents an object that can render a directory listing to a stream.
/// </summary>
public interface IDirectoryLister
{
/// <summary>
/// Gets the MIME type of generated directory listings.
/// </summary>
string ContentType { get; }
/// <summary>
/// Asynchronously generate a directory listing.
/// </summary>
/// <param name="info">A <see cref="MappedResourceInfo"/> containing information about
/// the directory which is to be listed.</param>
/// <param name="absoluteUrlPath">The absolute URL path that was mapped to <paramref name="info"/>.</param>
/// <param name="entries">An enumeration of the entries in the directory represented by <paramref name="info"/>.</param>
/// <param name="stream">A <see cref="Stream"/> to which the directory listing must be written.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
Task ListDirectoryAsync(
MappedResourceInfo info,
string absoluteUrlPath,
IEnumerable<MappedResourceInfo> entries,
Stream stream,
CancellationToken cancellationToken);
}
}

View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
namespace EmbedIO.Files
{
/// <summary>
/// Represents an object that can provide files and/or directories to be served by a <see cref="FileModule"/>.
/// </summary>
public interface IFileProvider
{
/// <summary>
/// <para>Occurs when a file or directory provided by this instance is modified or removed.</para>
/// <para>The event's parameter is the provider-specific path of the resource that changed.</para>
/// </summary>
event Action<string> ResourceChanged;
/// <summary>
/// Gets a value indicating whether the files and directories provided by this instance
/// will never change.
/// </summary>
bool IsImmutable { get; }
/// <summary>
/// Signals a file provider that the web server is starting.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to stop the web server.</param>
void Start(CancellationToken cancellationToken);
/// <summary>
/// Maps a URL path to a provider-specific path.
/// </summary>
/// <param name="urlPath">The URL path.</param>
/// <param name="mimeTypeProvider">An <see cref="IMimeTypeProvider"/> interface to use
/// for determining the MIME type of a file.</param>
/// <returns>A provider-specific path identifying a file or directory,
/// or <see langword="null"/> if this instance cannot provide a resource associated
/// to <paramref name="urlPath"/>.</returns>
MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider);
/// <summary>
/// Opens a file for reading.
/// </summary>
/// <param name="path">The provider-specific path for the file.</param>
/// <returns>
/// <para>A readable <see cref="Stream"/> of the file's contents.</para>
/// </returns>
Stream OpenFile(string path);
/// <summary>
/// Returns an enumeration of the entries of a directory.
/// </summary>
/// <param name="path">The provider-specific path for the directory.</param>
/// <param name="mimeTypeProvider">An <see cref="IMimeTypeProvider"/> interface to use
/// for determining the MIME type of files.</param>
/// <returns>An enumeration of <see cref="MappedResourceInfo"/> objects identifying the entries
/// in the directory identified by <paramref name="path"/>.</returns>
IEnumerable<MappedResourceInfo> GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<T> 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<FileCache.Section> _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<FileCache.Section>(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;
}
}

View File

@@ -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<IDirectoryLister> LazyInstance = new Lazy<IDirectoryLister>(() => 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<MappedResourceInfo> 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("<html><head><title>Index of ");
text.Write(encodedPath);
text.Write("</title></head><body><h1>Index of ");
text.Write(encodedPath);
text.Write("</h1><hr/><pre>");
if (encodedPath.Length > 1)
text.Write("<a href='../'>../</a>\n");
entries = entries.ToArray();
foreach (var directory in entries.Where(m => m.IsDirectory).OrderBy(e => e.Name))
{
text.Write($"<a href=\"{Uri.EscapeDataString(directory.Name)}\">{WebUtility.HtmlEncode(directory.Name)}</a>");
text.Write(new string(' ', Math.Max(1, MaxEntryLength - directory.Name.Length + 1)));
text.Write(HttpDate.Format(directory.LastModifiedUtc));
text.Write('\n');
await Task.Yield();
}
foreach (var file in entries.Where(m => m.IsFile).OrderBy(e => e.Name))
{
text.Write($"<a href=\"{Uri.EscapeDataString(file.Name)}\">{WebUtility.HtmlEncode(file.Name)}</a>");
text.Write(new string(' ', Math.Max(1, MaxEntryLength - file.Name.Length + 1)));
text.Write(HttpDate.Format(file.LastModifiedUtc));
text.Write($" {file.Length.ToString("#,###", CultureInfo.InvariantCulture),SizeIndent}\n");
await Task.Yield();
}
text.Write("</pre><hr/></body></html>");
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,80 @@
using System;
namespace EmbedIO.Files
{
/// <summary>
/// Contains information about a resource served via an <see cref="IFileProvider"/>.
/// </summary>
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;
}
/// <summary>
/// Gets a value indicating whether this instance represents a directory.
/// </summary>
public bool IsDirectory => ContentType == null;
/// <summary>
/// Gets a value indicating whether this instance represents a file.
/// </summary>
public bool IsFile => ContentType != null;
/// <summary>
/// Gets a unique, provider-specific path for the resource.
/// </summary>
public string Path { get; }
/// <summary>
/// Gets the name of the resource, as it would appear in a directory listing.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the UTC date and time of the last modification made to the resource.
/// </summary>
public DateTime LastModifiedUtc { get; }
/// <summary>
/// <para>If <see cref="IsDirectory"/> is <see langword="false"/>, gets the length of the file, expressed in bytes.</para>
/// <para>If <see cref="IsDirectory"/> is <see langword="true"/>, this property is always zero.</para>
/// </summary>
public long Length { get; }
/// <summary>
/// <para>If <see cref="IsDirectory"/> is <see langword="false"/>, gets a MIME type describing the kind of contents of the file.</para>
/// <para>If <see cref="IsDirectory"/> is <see langword="true"/>, this property is always <see langword="null"/>.</para>
/// </summary>
public string? ContentType { get; }
/// <summary>
/// Creates and returns a new instance of the <see cref="MappedResourceInfo"/> class,
/// representing a file.
/// </summary>
/// <param name="path">A unique, provider-specific path for the file.</param>
/// <param name="name">The name of the file, as it would appear in a directory listing.</param>
/// <param name="lastModifiedUtc">The UTC date and time of the last modification made to the file.</param>
/// <param name="size">The length of the file, expressed in bytes.</param>
/// <param name="contentType">A MIME type describing the kind of contents of the file.</param>
/// <returns>A newly-constructed instance of <see cref="MappedResourceInfo"/>.</returns>
public static MappedResourceInfo ForFile(string path, string name, DateTime lastModifiedUtc, long size, string contentType)
=> new MappedResourceInfo(path, name, lastModifiedUtc, size, contentType ?? MimeType.Default);
/// <summary>
/// Creates and returns a new instance of the <see cref="MappedResourceInfo"/> class,
/// representing a directory.
/// </summary>
/// <param name="path">A unique, provider-specific path for the directory.</param>
/// <param name="name">The name of the directory, as it would appear in a directory listing.</param>
/// <param name="lastModifiedUtc">The UTC date and time of the last modification made to the directory.</param>
/// <returns>A newly-constructed instance of <see cref="MappedResourceInfo"/>.</returns>
public static MappedResourceInfo ForDirectory(string path, string name, DateTime lastModifiedUtc)
=> new MappedResourceInfo(path, name, lastModifiedUtc, 0, null);
}
}

View File

@@ -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
{
/// <summary>
/// Provides access to embedded resources to a <see cref="FileModule"/>.
/// </summary>
/// <seealso cref="IFileProvider" />
public class ResourceFileProvider : IFileProvider
{
private readonly DateTime _fileTime = DateTime.UtcNow;
/// <summary>
/// Initializes a new instance of the <see cref="ResourceFileProvider"/> class.
/// </summary>
/// <param name="assembly">The assembly where served files are contained as embedded resources.</param>
/// <param name="pathPrefix">A string to prepend to provider-specific paths
/// to form the name of a manifest resource in <paramref name="assembly"/>.</param>
/// <exception cref="ArgumentNullException"><paramref name="assembly"/> is <see langword="null"/>.</exception>
public ResourceFileProvider(Assembly assembly, string pathPrefix)
{
Assembly = Validate.NotNull(nameof(assembly), assembly);
PathPrefix = pathPrefix ?? string.Empty;
}
/// <inheritdoc />
public event Action<string> ResourceChanged
{
add { }
remove { }
}
/// <summary>
/// Gets the assembly where served files are contained as embedded resources.
/// </summary>
public Assembly Assembly { get; }
/// <summary>
/// Gets a string that is prepended to provider-specific paths to form the name of a manifest resource in <see cref="Assembly"/>.
/// </summary>
public string PathPrefix { get; }
/// <inheritdoc />
public bool IsImmutable => true;
/// <inheritdoc />
public void Start(CancellationToken cancellationToken)
{
}
/// <inheritdoc />
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)));
}
/// <inheritdoc />
public Stream OpenFile(string path) => Assembly.GetManifestResourceStream(path);
/// <inheritdoc />
public IEnumerable<MappedResourceInfo> GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider)
=> Enumerable.Empty<MappedResourceInfo>();
}
}

View File

@@ -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
{
/// <summary>
/// Provides access to files contained in a <c>.zip</c> file to a <see cref="FileModule"/>.
/// </summary>
/// <seealso cref="IFileProvider" />
public class ZipFileProvider : IDisposable, IFileProvider
{
private readonly ZipArchive _zipArchive;
/// <summary>
/// Initializes a new instance of the <see cref="ZipFileProvider"/> class.
/// </summary>
/// <param name="zipFilePath">The zip file path.</param>
public ZipFileProvider(string zipFilePath)
: this(new FileStream(Validate.LocalPath(nameof(zipFilePath), zipFilePath, true), FileMode.Open))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ZipFileProvider"/> class.
/// </summary>
/// <param name="stream">The stream that contains the archive.</param>
/// <param name="leaveOpen"><see langword="true"/> to leave the stream open after the web server
/// is disposed; otherwise, <see langword="false"/>.</param>
public ZipFileProvider(Stream stream, bool leaveOpen = false)
{
_zipArchive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen);
}
/// <summary>
/// Finalizes an instance of the <see cref="ZipFileProvider"/> class.
/// </summary>
~ZipFileProvider()
{
Dispose(false);
}
/// <inheritdoc />
public event Action<string> ResourceChanged
{
add { }
remove { }
}
/// <inheritdoc />
public bool IsImmutable => true;
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public void Start(CancellationToken cancellationToken)
{
}
/// <inheritdoc />
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)));
}
/// <inheritdoc />
public Stream OpenFile(string path)
=> _zipArchive.GetEntry(path)?.Open() ?? throw new FileNotFoundException($"\"{path}\" cannot be found in Zip archive.");
/// <inheritdoc />
public IEnumerable<MappedResourceInfo> GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider)
=> Enumerable.Empty<MappedResourceInfo>();
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources;
/// <see langword="false"/> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!disposing)
return;
_zipArchive.Dispose();
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
namespace EmbedIO
{
partial class HttpContextExtensions
{
/// <summary>Gets the item associated with the specified key.</summary>
/// <typeparam name="T">The desired type of the item.</typeparam>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <param name="key">The key whose value to get from the <see cref="IHttpContext.Items">Items</see> dictionary.</param>
/// <param name="value">
/// <para>When this method returns, the item associated with the specified key,
/// if the key is found in <see cref="IHttpContext.Items">Items</see>
/// and the associated value is of type <typeparamref name="T"/>;
/// otherwise, the default value for <typeparamref name="T"/>.</para>
/// <para>This parameter is passed uninitialized.</para>
/// </param>
/// <returns><see langword="true"/> if the item is found and is of type <typeparamref name="T"/>;
/// otherwise, <see langword="false"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"/>.</exception>
public static bool TryGetItem<T>(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;
}
/// <summary>Gets the item associated with the specified key.</summary>
/// <typeparam name="T">The desired type of the item.</typeparam>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <param name="key">The key whose value to get from the <see cref="IHttpContext.Items">Items</see> dictionary.</param>
/// <returns>The item associated with the specified key,
/// if the key is found in <see cref="IHttpContext.Items">Items</see>
/// and the associated value is of type <typeparamref name="T"/>;
/// otherwise, the default value for <typeparamref name="T"/>.</returns>
public static T GetItem<T>(this IHttpContext @this, object key)
=> @this.Items.TryGetValue(key, out var item) && item is T typedItem ? typedItem : default;
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Net;
using EmbedIO.Utilities;
namespace EmbedIO
{
partial class HttpContextExtensions
{
/// <summary>
/// Sets a redirection status code and adds a <c>Location</c> header to the response.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> interface on which this method is called.</param>
/// <param name="location">The URL to which the user agent should be redirected.</param>
/// <param name="statusCode">The status code to set on the response.</param>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="location"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para><paramref name="location"/> is not a valid relative or absolute URL.<see langword="null"/>.</para>
/// <para>- or -</para>
/// <para><paramref name="statusCode"/> is not a redirection (3xx) status code.</para>
/// </exception>
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;
}
}
}

View File

@@ -0,0 +1,61 @@
using System.IO;
using System.IO.Compression;
using System.Text;
using Swan.Logging;
namespace EmbedIO
{
partial class HttpContextExtensions
{
/// <summary>
/// <para>Wraps the request input stream and returns a <see cref="Stream"/> that can be used directly.</para>
/// <para>Decompression of compressed request bodies is implemented if specified in the web server's options.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <returns>
/// <para>A <see cref="Stream"/> that can be used to write response data.</para>
/// <para>This stream MUST be disposed when finished writing.</para>
/// </returns>
/// <seealso cref="OpenRequestText"/>
/// <seealso cref="WebServerOptionsBase.SupportCompressedRequests"/>
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}\"");
}
/// <summary>
/// <para>Wraps the request input stream and returns a <see cref="TextReader" /> that can be used directly.</para>
/// <para>Decompression of compressed request bodies is implemented if specified in the web server's options.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpContext" /> on which this method is called.</param>
/// <returns>
/// <para>A <see cref="TextReader" /> that can be used to read the request body as text.</para>
/// <para>This reader MUST be disposed when finished reading.</para>
/// </returns>
/// <seealso cref="OpenRequestStream"/>
/// <seealso cref="WebServerOptionsBase.SupportCompressedRequests"/>
public static TextReader OpenRequestText(this IHttpContext @this)
=> new StreamReader(OpenRequestStream(@this), @this.Request.ContentEncoding);
}
}

View File

@@ -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();
/// <summary>
/// Asynchronously retrieves the request body as an array of <see langword="byte"/>s.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <returns>A <see cref="Task{TResult}">Task</see>, representing the ongoing operation,
/// whose result will be an array of <see cref="byte"/>s containing the request body.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
public static async Task<byte[]> 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();
}
/// <summary>
/// Asynchronously buffers the request body into a read-only <see cref="MemoryStream"/>.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <returns>A <see cref="Task{TResult}">Task</see>, representing the ongoing operation,
/// whose result will be a read-only <see cref="MemoryStream"/> containing the request body.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
public static async Task<MemoryStream> GetRequestBodyAsMemoryStreamAsync(this IHttpContext @this)
=> new MemoryStream(
await GetRequestBodyAsByteArrayAsync(@this).ConfigureAwait(false),
false);
/// <summary>
/// Asynchronously retrieves the request body as a string.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <returns>A <see cref="Task{TResult}">Task</see>, representing the ongoing operation,
/// whose result will be a <see langword="string"/> representation of the request body.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
public static async Task<string> GetRequestBodyAsStringAsync(this IHttpContext @this)
{
using var reader = @this.OpenRequestText();
return await reader.ReadToEndAsync().ConfigureAwait(false);
}
/// <summary>
/// <para>Asynchronously deserializes a request body, using the default request deserializer.</para>
/// <para>As of EmbedIO version 3.0, the default response serializer has the same behavior of JSON
/// request parsing methods of version 2.</para>
/// </summary>
/// <typeparam name="TData">The expected type of the deserialized data.</typeparam>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <returns>A <see cref="Task{TResult}">Task</see>, representing the ongoing operation,
/// whose result will be the deserialized data.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
public static Task<TData> GetRequestDataAsync<TData>(this IHttpContext @this)
=> RequestDeserializer.Default<TData>(@this);
/// <summary>
/// Asynchronously deserializes a request body, using the specified request deserializer.
/// </summary>
/// <typeparam name="TData">The expected type of the deserialized data.</typeparam>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <param name="deserializer">A <see cref="RequestDeserializerCallback{TData}"/> used to deserialize the request body.</param>
/// <returns>A <see cref="Task{TResult}">Task</see>, representing the ongoing operation,
/// whose result will be the deserialized data.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="deserializer"/> is <see langword="null"/>.</exception>
public static Task<TData> GetRequestDataAsync<TData>(this IHttpContext @this,RequestDeserializerCallback<TData> deserializer)
=> Validate.NotNull(nameof(deserializer), deserializer)(@this);
/// <summary>
/// Asynchronously parses a request body in <c>application/x-www-form-urlencoded</c> format.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <returns>A <see cref="Task{TResult}">Task</see>, representing the ongoing operation,
/// whose result will be a read-only <see cref="NameValueCollection"/>of form field names and values.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>This method may safely be called more than once for the same <see cref="IHttpContext"/>:
/// it will return the same collection instead of trying to parse the request body again.</para>
/// </remarks>
public static async Task<NameValueCollection> 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}");
}
}
/// <summary>
/// Parses a request URL query. Note that this is different from getting the <see cref="IHttpRequest.QueryString"/> 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 <c>null</c> key.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <returns>A read-only <see cref="NameValueCollection"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>This method may safely be called more than once for the same <see cref="IHttpContext"/>:
/// it will return the same collection instead of trying to parse the request body again.</para>
/// </remarks>
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}");
}
}
}
}

View File

@@ -0,0 +1,68 @@
using System.IO;
using System.IO.Compression;
using System.Text;
using EmbedIO.Internal;
namespace EmbedIO
{
partial class HttpContextExtensions
{
/// <summary>
/// <para>Wraps the response output stream and returns a <see cref="Stream"/> that can be used directly.</para>
/// <para>Optional buffering is applied, so that the response may be sent as one instead of using chunked transfer.</para>
/// <para>Proactive negotiation is performed to select the best compression method supported by the client.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <param name="buffered">If set to <see langword="true"/>, sent data is collected
/// in a <see cref="MemoryStream"/> and sent all at once when the returned <see cref="Stream"/>
/// is disposed; if set to <see langword="false"/> (the default), chunked transfer will be used.</param>
/// <param name="preferCompression"><see langword="true"/> if sending compressed data is preferred over
/// sending non-compressed data; otherwise, <see langword="false"/>.</param>
/// <returns>
/// <para>A <see cref="Stream"/> that can be used to write response data.</para>
/// <para>This stream MUST be disposed when finished writing.</para>
/// </returns>
/// <seealso cref="OpenResponseText"/>
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
};
}
/// <summary>
/// <para>Wraps the response output stream and returns a <see cref="TextWriter" /> that can be used directly.</para>
/// <para>Optional buffering is applied, so that the response may be sent as one instead of using chunked transfer.</para>
/// <para>Proactive negotiation is performed to select the best compression method supported by the client.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpContext" /> on which this method is called.</param>
/// <param name="encoding">
/// <para>The <see cref="Encoding"/> to use to convert text to data bytes.</para>
/// <para>If <see langword="null"/> (the default), <see cref="WebServer.DefaultEncoding"/> (UTF-8 without a byte order mark) is used.</para>
/// </param>
/// <param name="buffered">If set to <see langword="true" />, sent data is collected
/// in a <see cref="MemoryStream" /> and sent all at once when the returned <see cref="Stream" />
/// is disposed; if set to <see langword="false" /> (the default), chunked transfer will be used.</param>
/// <param name="preferCompression"><see langword="true"/> if sending compressed data is preferred over
/// sending non-compressed data; otherwise, <see langword="false"/>.</param>
/// <returns>
/// <para>A <see cref="TextWriter" /> that can be used to write response data.</para>
/// <para>This writer MUST be disposed when finished writing.</para>
/// </returns>
/// <seealso cref="OpenResponseStream"/>
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);
}
}
}

View File

@@ -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 = "<html><head><meta charset=\"{2}\"><title>{0} - {1}</title></head><body><h1>{0} - {1}</h1>";
private const string StandardHtmlFooter = "</body></html>";
/// <summary>
/// Asynchronously sends a string as response.
/// </summary>
/// <param name="this">The <see cref="IHttpResponse"/> interface on which this method is called.</param>
/// <param name="content">The response content.</param>
/// <param name="contentType">The MIME type of the content. If <see langword="null"/>, the content type will not be set.</param>
/// <param name="encoding">The <see cref="Encoding"/> to use.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException">
/// <para><paramref name="content"/> is <see langword="null"/>.</para>
/// <para>- or -</para>
/// <para><paramref name="encoding"/> is <see langword="null"/>.</para>
/// </exception>
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);
}
/// <summary>
/// Asynchronously sends a standard HTML response for the specified status code.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> interface on which this method is called.</param>
/// <param name="statusCode">The HTTP status code of the response.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">There is no standard status description for <paramref name="statusCode"/>.</exception>
/// <seealso cref="SendStandardHtmlAsync(IHttpContext,int,Action{TextWriter})"/>
public static Task SendStandardHtmlAsync(this IHttpContext @this, int statusCode)
=> SendStandardHtmlAsync(@this, statusCode, null);
/// <summary>
/// Asynchronously sends a standard HTML response for the specified status code.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> interface on which this method is called.</param>
/// <param name="statusCode">The HTTP status code of the response.</param>
/// <param name="writeAdditionalHtml">A callback function that may write additional HTML code
/// to a <see cref="TextWriter"/> representing the response output.
/// If not <see langword="null"/>, the callback is called immediately before closing the HTML <c>body</c> tag.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">There is no standard status description for <paramref name="statusCode"/>.</exception>
/// <seealso cref="SendStandardHtmlAsync(IHttpContext,int)"/>
public static Task SendStandardHtmlAsync(
this IHttpContext @this,
int statusCode,
Action<TextWriter>? 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;
}
/// <summary>
/// <para>Asynchronously sends serialized data as a response, using the default response serializer.</para>
/// <para>As of EmbedIO version 3.0, the default response serializer has the same behavior of JSON
/// response methods of version 2.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> interface on which this method is called.</param>
/// <param name="data">The data to serialize.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <seealso cref="SendDataAsync(IHttpContext,ResponseSerializerCallback,object)"/>
/// <seealso cref="ResponseSerializer.Default"/>
public static Task SendDataAsync(this IHttpContext @this, object data)
=> ResponseSerializer.Default(@this, data);
/// <summary>
/// <para>Asynchronously sends serialized data as a response, using the specified response serializer.</para>
/// <para>As of EmbedIO version 3.0, the default response serializer has the same behavior of JSON
/// response methods of version 2.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> interface on which this method is called.</param>
/// <param name="serializer">A <see cref="ResponseSerializerCallback"/> used to prepare the response.</param>
/// <param name="data">The data to serialize.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="serializer"/> is <see langword="null"/>.</exception>
/// <seealso cref="SendDataAsync(IHttpContext,ResponseSerializerCallback,object)"/>
/// <seealso cref="ResponseSerializer.Default"/>
public static Task SendDataAsync(this IHttpContext @this, ResponseSerializerCallback serializer, object data)
=> Validate.NotNull(nameof(serializer), serializer)(@this, data);
}
}

View File

@@ -0,0 +1,30 @@
using System;
using EmbedIO.Utilities;
using Swan;
namespace EmbedIO
{
/// <summary>
/// Provides extension methods for types implementing <see cref="IHttpContext"/>.
/// </summary>
public static partial class HttpContextExtensions
{
/// <summary>
/// <para>Gets the underlying <see cref="IHttpContextImpl"/> interface of an <see cref="IHttpContext"/>.</para>
/// <para>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.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> interface on which this method is called.</param>
/// <returns>The underlying <see cref="IHttpContextImpl"/> interface representing
/// the HTTP context implementation.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="this"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="EmbedIOInternalErrorException">
/// <paramref name="this"/> does not implement <see cref="IHttpContextImpl"/>.
/// </exception>
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)}.");
}
}

View File

@@ -0,0 +1,158 @@
using System;
using System.Net;
namespace EmbedIO
{
partial class HttpException
{
/// <summary>
/// Returns a new instance of <see cref="HttpException" /> that, when thrown,
/// will break the request handling control flow and send a <c>500 Internal Server Error</c>
/// response to the client.
/// </summary>
/// <param name="message">A message to include in the response.</param>
/// <param name="data">The data object to include in the response.</param>
/// <returns>
/// A newly-created <see cref="HttpException" />.
/// </returns>
public static HttpException InternalServerError(string? message = null, object? data = null)
=> new HttpException(HttpStatusCode.InternalServerError, message, data);
/// <summary>
/// Returns a new instance of <see cref="HttpException" /> that, when thrown,
/// will break the request handling control flow and send a <c>401 Unauthorized</c>
/// response to the client.
/// </summary>
/// <param name="message">A message to include in the response.</param>
/// <param name="data">The data object to include in the response.</param>
/// <returns>
/// A newly-created <see cref="HttpException" />.
/// </returns>
public static HttpException Unauthorized(string? message = null, object? data = null)
=> new HttpException(HttpStatusCode.Unauthorized, message, data);
/// <summary>
/// Returns a new instance of <see cref="HttpException"/> that, when thrown,
/// will break the request handling control flow and send a <c>403 Forbidden</c>
/// response to the client.
/// </summary>
/// <param name="message">A message to include in the response.</param>
/// <param name="data">The data object to include in the response.</param>
/// <returns>A newly-created <see cref="HttpException"/>.</returns>
public static HttpException Forbidden(string? message = null, object? data = null)
=> new HttpException(HttpStatusCode.Forbidden, message, data);
/// <summary>
/// Returns a new instance of <see cref="HttpException"/> that, when thrown,
/// will break the request handling control flow and send a <c>400 Bad Request</c>
/// response to the client.
/// </summary>
/// <param name="message">A message to include in the response.</param>
/// <param name="data">The data object to include in the response.</param>
/// <returns>A newly-created <see cref="HttpException"/>.</returns>
public static HttpException BadRequest(string? message = null, object? data = null)
=> new HttpException(HttpStatusCode.BadRequest, message, data);
/// <summary>
/// Returns a new instance of <see cref="HttpException"/> that, when thrown,
/// will break the request handling control flow and send a <c>404 Not Found</c>
/// response to the client.
/// </summary>
/// <param name="message">A message to include in the response.</param>
/// <param name="data">The data object to include in the response.</param>
/// <returns>A newly-created <see cref="HttpException"/>.</returns>
public static HttpException NotFound(string? message = null, object? data = null)
=> new HttpException(HttpStatusCode.NotFound, message, data);
/// <summary>
/// Returns a new instance of <see cref="HttpException"/> that, when thrown,
/// will break the request handling control flow and send a <c>405 Method Not Allowed</c>
/// response to the client.
/// </summary>
/// <param name="message">A message to include in the response.</param>
/// <param name="data">The data object to include in the response.</param>
/// <returns>A newly-created <see cref="HttpException"/>.</returns>
public static HttpException MethodNotAllowed(string? message = null, object? data = null)
=> new HttpException(HttpStatusCode.MethodNotAllowed, message, data);
/// <summary>
/// Returns a new instance of <see cref="HttpNotAcceptableException"/> that, when thrown,
/// will break the request handling control flow and send a <c>406 Not Acceptable</c>
/// response to the client.
/// </summary>
/// <returns>A newly-created <see cref="HttpNotAcceptableException"/>.</returns>
/// <seealso cref="HttpNotAcceptableException()"/>
public static HttpNotAcceptableException NotAcceptable() => new HttpNotAcceptableException();
/// <summary>
/// <para>Returns a new instance of <see cref="HttpNotAcceptableException"/> that, when thrown,
/// will break the request handling control flow and send a <c>406 Not Acceptable</c>
/// response to the client.</para>
/// </summary>
/// <param name="vary">A value, or a comma-separated list of values, to set the response's <c>Vary</c> header to.</param>
/// <returns>A newly-created <see cref="HttpNotAcceptableException"/>.</returns>
/// <seealso cref="HttpNotAcceptableException(string)"/>
public static HttpNotAcceptableException NotAcceptable(string vary) => new HttpNotAcceptableException(vary);
/// <summary>
/// Returns a new instance of <see cref="HttpRangeNotSatisfiableException"/> that, when thrown,
/// will break the request handling control flow and send a <c>416 Range Not Satisfiable</c>
/// response to the client.
/// </summary>
/// <returns>A newly-created <see cref="HttpRangeNotSatisfiableException"/>.</returns>
/// <seealso cref="HttpRangeNotSatisfiableException()"/>
public static HttpRangeNotSatisfiableException RangeNotSatisfiable() => new HttpRangeNotSatisfiableException();
/// <summary>
/// Returns a new instance of <see cref="HttpRangeNotSatisfiableException"/> that, when thrown,
/// will break the request handling control flow and send a <c>416 Range Not Satisfiable</c>
/// response to the client.
/// </summary>
/// <param name="contentLength">The total length of the requested resource, expressed in bytes,
/// or <see langword="null"/> to omit the <c>Content-Range</c> header in the response.</param>
/// <returns>A newly-created <see cref="HttpRangeNotSatisfiableException"/>.</returns>
/// <seealso cref="HttpRangeNotSatisfiableException()"/>
public static HttpRangeNotSatisfiableException RangeNotSatisfiable(long? contentLength)
=> new HttpRangeNotSatisfiableException(contentLength);
/// <summary>
/// Returns a new instance of <see cref="HttpRedirectException" /> that, when thrown,
/// will break the request handling control flow and redirect the client
/// to the specified location, using response status code 302.
/// </summary>
/// <param name="location">The redirection target.</param>
/// <returns>
/// A newly-created <see cref="HttpRedirectException" />.
/// </returns>
public static HttpRedirectException Redirect(string location)
=> new HttpRedirectException(location);
/// <summary>
/// Returns a new instance of <see cref="HttpRedirectException" /> that, when thrown,
/// will break the request handling control flow and redirect the client
/// to the specified location, using the specified response status code.
/// </summary>
/// <param name="location">The redirection target.</param>
/// <param name="statusCode">The status code to set on the response, in the range from 300 to 399.</param>
/// <returns>
/// A newly-created <see cref="HttpRedirectException" />.
/// </returns>
/// <exception cref="ArgumentException"><paramref name="statusCode"/> is not in the 300-399 range.</exception>
public static HttpRedirectException Redirect(string location, int statusCode)
=> new HttpRedirectException(location, statusCode);
/// <summary>
/// Returns a new instance of <see cref="HttpRedirectException" /> that, when thrown,
/// will break the request handling control flow and redirect the client
/// to the specified location, using the specified response status code.
/// </summary>
/// <param name="location">The redirection target.</param>
/// <param name="statusCode">One of the redirection status codes, to be set on the response.</param>
/// <returns>
/// A newly-created <see cref="HttpRedirectException" />.
/// </returns>
/// <exception cref="ArgumentException"><paramref name="statusCode"/> is not a redirection status code.</exception>
public static HttpRedirectException Redirect(string location, HttpStatusCode statusCode)
=> new HttpRedirectException(location, statusCode);
}
}

105
Vendor/EmbedIO-3.5.2/HttpException.cs vendored Normal file
View File

@@ -0,0 +1,105 @@
using System;
using System.Net;
namespace EmbedIO
{
/// <summary>
/// When thrown, breaks the request handling control flow
/// and sends an error response to the client.
/// </summary>
#pragma warning disable CA1032 // Implement standard exception constructors - they have no meaning here.
public partial class HttpException : Exception, IHttpException
#pragma warning restore CA1032
{
/// <summary>
/// Initializes a new instance of the <see cref="HttpException"/> class,
/// with no message to include in the response.
/// </summary>
/// <param name="statusCode">The status code to set on the response.</param>
public HttpException(int statusCode)
{
StatusCode = statusCode;
}
/// <summary>
/// Initializes a new instance of the <see cref="HttpException"/> class,
/// with no message to include in the response.
/// </summary>
/// <param name="statusCode">The status code to set on the response.</param>
public HttpException(HttpStatusCode statusCode)
: this((int)statusCode)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="HttpException"/> class,
/// with a message to include in the response.
/// </summary>
/// <param name="statusCode">The status code to set on the response.</param>
/// <param name="message">A message to include in the response as plain text.</param>
public HttpException(int statusCode, string? message)
: base(message)
{
StatusCode = statusCode;
HttpExceptionMessage = message;
}
/// <summary>
/// Initializes a new instance of the <see cref="HttpException"/> class,
/// with a message to include in the response.
/// </summary>
/// <param name="statusCode">The status code to set on the response.</param>
/// <param name="message">A message to include in the response as plain text.</param>
public HttpException(HttpStatusCode statusCode, string? message)
: this((int)statusCode, message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="HttpException" /> class,
/// with a message and a data object to include in the response.
/// </summary>
/// <param name="statusCode">The status code to set on the response.</param>
/// <param name="message">A message to include in the response as plain text.</param>
/// <param name="data">The data object to include in the response.</param>
public HttpException(int statusCode, string? message, object? data)
: this(statusCode, message)
{
DataObject = data;
}
/// <summary>
/// Initializes a new instance of the <see cref="HttpException" /> class,
/// with a message and a data object to include in the response.
/// </summary>
/// <param name="statusCode">The status code to set on the response.</param>
/// <param name="message">A message to include in the response as plain text.</param>
/// <param name="data">The data object to include in the response.</param>
public HttpException(HttpStatusCode statusCode, string? message, object? data)
: this((int)statusCode, message, data)
{
}
/// <inheritdoc />
public int StatusCode { get; }
/// <inheritdoc />
public object? DataObject { get; }
/// <inheritdoc />
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; }
/// <inheritdoc />
/// <remarks>
/// <para>This method does nothing; there is no need to call
/// <c>base.PrepareResponse</c> in overrides of this method.</para>
/// </remarks>
public virtual void PrepareResponse(IHttpContext context)
{
}
}
}

View File

@@ -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
{
/// <summary>
/// Provides standard handlers for HTTP exceptions at both module and server level.
/// </summary>
/// <remarks>
/// <para>Where applicable, HTTP exception handlers defined in this class
/// use the <see cref="ExceptionHandler.ContactInformation"/> and
/// <see cref="ExceptionHandler.IncludeStackTraces"/> properties to customize
/// their behavior.</para>
/// </remarks>
/// <seealso cref="IWebServer.OnHttpException"/>
/// <seealso cref="IWebModule.OnHttpException"/>
public static class HttpExceptionHandler
{
/// <summary>
/// <para>Gets the default handler used by <see cref="WebServerBase{TOptions}"/>.</para>
/// <para>This is the same as <see cref="HtmlResponse"/>.</para>
/// </summary>
public static HttpExceptionHandlerCallback Default { get; } = HtmlResponse;
/// <summary>
/// Sends an empty response.
/// </summary>
/// <param name="context">An <see cref="IHttpContext" /> interface representing the context of the request.</param>
/// <param name="httpException">The HTTP exception.</param>
/// <returns>A <see cref="Task" /> representing the ongoing operation.</returns>
#pragma warning disable CA1801 // Unused parameter
public static Task EmptyResponse(IHttpContext context, IHttpException httpException)
#pragma warning restore CA1801
=> Task.CompletedTask;
/// <summary>
/// <para>Sends a HTTP exception's <see cref="IHttpException.Message">Message</see> property
/// as a plain text response.</para>
/// <para>This handler does not use the <see cref="IHttpException.DataObject">DataObject</see> property.</para>
/// </summary>
/// <param name="context">An <see cref="IHttpContext" /> interface representing the context of the request.</param>
/// <param name="httpException">The HTTP exception.</param>
/// <returns>A <see cref="Task" /> representing the ongoing operation.</returns>
public static Task PlainTextResponse(IHttpContext context, IHttpException httpException)
=> context.SendStringAsync(httpException.Message ?? string.Empty, MimeType.PlainText, WebServer.DefaultEncoding);
/// <summary>
/// <para>Sends a response with a HTML payload
/// briefly describing the error, including contact information and/or a stack trace
/// if specified via the <see cref="ExceptionHandler.ContactInformation"/>
/// and <see cref="ExceptionHandler.IncludeStackTraces"/> properties, respectively.</para>
/// <para>This handler does not use the <see cref="IHttpException.DataObject">DataObject</see> property.</para>
/// </summary>
/// <param name="context">An <see cref="IHttpContext" /> interface representing the context of the request.</param>
/// <param name="httpException">The HTTP exception.</param>
/// <returns>A <see cref="Task" /> representing the ongoing operation.</returns>
public static Task HtmlResponse(IHttpContext context, IHttpException httpException)
=> context.SendStandardHtmlAsync(
httpException.StatusCode,
text => {
text.Write(
"<p><strong>Exception type:</strong> {0}<p><strong>Message:</strong> {1}",
WebUtility.HtmlEncode(httpException.GetType().FullName ?? "<unknown>"),
WebUtility.HtmlEncode(httpException.Message));
text.Write("<hr><p>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.</p>");
if (ExceptionHandler.IncludeStackTraces)
{
text.Write(
"</p><p><strong>Stack trace:</strong></p><br><pre>{0}</pre>",
WebUtility.HtmlEncode(httpException.StackTrace));
}
});
/// <summary>
/// <para>Gets a <see cref="HttpExceptionHandlerCallback" /> that will serialize a HTTP exception's
/// <see cref="IHttpException.DataObject">DataObject</see> property and send it as a JSON response.</para>
/// </summary>
/// <param name="serializerCallback">A <see cref="ResponseSerializerCallback" /> used to serialize data and send it to the client.</param>
/// <returns>A <see cref="HttpExceptionHandlerCallback" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="serializerCallback"/> is <see langword="null"/>.</exception>
public static HttpExceptionHandlerCallback DataResponse(ResponseSerializerCallback serializerCallback)
{
Validate.NotNull(nameof(serializerCallback), serializerCallback);
return (context, httpException) => serializerCallback(context, httpException.DataObject);
}
/// <summary>
/// <para>Gets a <see cref="HttpExceptionHandlerCallback" /> that will serialize a HTTP exception's
/// <see cref="IHttpException.Message">Message</see> and <see cref="IHttpException.DataObject">DataObject</see> properties
/// and send them as a JSON response.</para>
/// <para>The response will be a JSON object with a <c>message</c> property and a <c>data</c> property.</para>
/// </summary>
/// <param name="serializerCallback">A <see cref="ResponseSerializerCallback" /> used to serialize data and send it to the client.</param>
/// <returns>A <see cref="HttpExceptionHandlerCallback" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="serializerCallback"/> is <see langword="null"/>.</exception>
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}");
}
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Threading.Tasks;
namespace EmbedIO
{
/// <summary>
/// A callback used to build the contents of the response for an <see cref="IHttpException" />.
/// </summary>
/// <param name="context">An <see cref="IHttpContext" /> interface representing the context of the request.</param>
/// <param name="httpException">An <see cref="IHttpException" /> interface.</param>
/// <returns>A <see cref="Task" /> representing the ongoing operation.</returns>
/// <remarks>
/// <para>When this delegate is called, the response's status code has already been set and the <see cref="IHttpException.PrepareResponse"/>
/// method has already been called. The only thing left to do is preparing the response's content, according
/// to the <see cref="IHttpException.Message"/> property.</para>
/// <para>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 <c>throw HttpException.NotFound()</c> (or similar)
/// from a handler.</para>
/// </remarks>
public delegate Task HttpExceptionHandlerCallback(IHttpContext context, IHttpException httpException);
}

449
Vendor/EmbedIO-3.5.2/HttpHeaderNames.cs vendored Normal file
View File

@@ -0,0 +1,449 @@
namespace EmbedIO
{
/// <summary>
/// Exposes known HTTP header names.
/// </summary>
/// <remarks>
/// <para>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.</para>
/// </remarks>
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.
/// <summary>
/// The <c>Accept</c> HTTP header.
/// </summary>
public const string Accept = "Accept";
/// <summary>
/// The <c>Accept-Charset</c> HTTP header.
/// </summary>
public const string AcceptCharset = "Accept-Charset";
/// <summary>
/// The <c>Accept-Encoding</c> HTTP header.
/// </summary>
public const string AcceptEncoding = "Accept-Encoding";
/// <summary>
/// The <c>Accept-Language</c> HTTP header.
/// </summary>
public const string AcceptLanguage = "Accept-Language";
/// <summary>
/// The <c>Accept-Patch</c> HTTP header.
/// </summary>
public const string AcceptPatch = "Accept-Patch";
/// <summary>
/// The <c>Accept-Ranges</c> HTTP header.
/// </summary>
public const string AcceptRanges = "Accept-Ranges";
/// <summary>
/// The <c>Access-Control-Allow-Credentials</c> HTTP header.
/// </summary>
public const string AccessControlAllowCredentials = "Access-Control-Allow-Credentials";
/// <summary>
/// The <c>Access-Control-Allow-Headers</c> HTTP header.
/// </summary>
public const string AccessControlAllowHeaders = "Access-Control-Allow-Headers";
/// <summary>
/// The <c>Access-Control-Allow-Methods</c> HTTP header.
/// </summary>
public const string AccessControlAllowMethods = "Access-Control-Allow-Methods";
/// <summary>
/// The <c>Access-Control-Allow-Origin</c> HTTP header.
/// </summary>
public const string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
/// <summary>
/// The <c>Access-Control-Expose-Headers</c> HTTP header.
/// </summary>
public const string AccessControlExposeHeaders = "Access-Control-Expose-Headers";
/// <summary>
/// The <c>Access-Control-Max-Age</c> HTTP header.
/// </summary>
public const string AccessControlMaxAge = "Access-Control-Max-Age";
/// <summary>
/// The <c>Access-Control-Request-Headers</c> HTTP header.
/// </summary>
public const string AccessControlRequestHeaders = "Access-Control-Request-Headers";
/// <summary>
/// The <c>Access-Control-Request-Method</c> HTTP header.
/// </summary>
public const string AccessControlRequestMethod = "Access-Control-Request-Method";
/// <summary>
/// The <c>Age</c> HTTP header.
/// </summary>
public const string Age = "Age";
/// <summary>
/// The <c>Allow</c> HTTP header.
/// </summary>
public const string Allow = "Allow";
/// <summary>
/// The <c>Alt-Svc</c> HTTP header.
/// </summary>
public const string AltSvc = "Alt-Svc";
/// <summary>
/// The <c>Authorization</c> HTTP header.
/// </summary>
public const string Authorization = "Authorization";
/// <summary>
/// The <c>Cache-Control</c> HTTP header.
/// </summary>
public const string CacheControl = "Cache-Control";
/// <summary>
/// The <c>Connection</c> HTTP header.
/// </summary>
public const string Connection = "Connection";
/// <summary>
/// The <c>Content-Disposition</c> HTTP header.
/// </summary>
public const string ContentDisposition = "Content-Disposition";
/// <summary>
/// The <c>Content-Encoding</c> HTTP header.
/// </summary>
public const string ContentEncoding = "Content-Encoding";
/// <summary>
/// The <c>Content-Language</c> HTTP header.
/// </summary>
public const string ContentLanguage = "Content-Language";
/// <summary>
/// The <c>Content-Length</c> HTTP header.
/// </summary>
public const string ContentLength = "Content-Length";
/// <summary>
/// The <c>Content-Location</c> HTTP header.
/// </summary>
public const string ContentLocation = "Content-Location";
/// <summary>
/// The <c>Content-MD5</c> HTTP header.
/// </summary>
public const string ContentMD5 = "Content-MD5";
/// <summary>
/// The <c>Content-Range</c> HTTP header.
/// </summary>
public const string ContentRange = "Content-Range";
/// <summary>
/// The <c>Content-Security-Policy</c> HTTP header.
/// </summary>
public const string ContentSecurityPolicy = "Content-Security-Policy";
/// <summary>
/// The <c>Content-Type</c> HTTP header.
/// </summary>
public const string ContentType = "Content-Type";
/// <summary>
/// The <c>Cookie</c> HTTP header.
/// </summary>
public const string Cookie = "Cookie";
/// <summary>
/// The <c>Cookie2</c> HTTP header.
/// </summary>
public const string Cookie2 = "Cookie2";
/// <summary>
/// The <c>Date</c> HTTP header.
/// </summary>
public const string Date = "Date";
/// <summary>
/// The <c>ETag</c> HTTP header.
/// </summary>
public const string ETag = "ETag";
/// <summary>
/// The <c>Expect</c> HTTP header.
/// </summary>
public const string Expect = "Expect";
/// <summary>
/// The <c>Expires</c> HTTP header.
/// </summary>
public const string Expires = "Expires";
/// <summary>
/// The <c>From</c> HTTP header.
/// </summary>
public const string From = "From";
/// <summary>
/// The <c>Host</c> HTTP header.
/// </summary>
public const string Host = "Host";
/// <summary>
/// The <c>If-Match</c> HTTP header.
/// </summary>
public const string IfMatch = "If-Match";
/// <summary>
/// The <c>If-Modified-Since</c> HTTP header.
/// </summary>
public const string IfModifiedSince = "If-Modified-Since";
/// <summary>
/// The <c>If-None-Match</c> HTTP header.
/// </summary>
public const string IfNoneMatch = "If-None-Match";
/// <summary>
/// The <c>If-Range</c> HTTP header.
/// </summary>
public const string IfRange = "If-Range";
/// <summary>
/// The <c>If-Unmodified-Since</c> HTTP header.
/// </summary>
public const string IfUnmodifiedSince = "If-Unmodified-Since";
/// <summary>
/// The <c>Keep-Alive</c> HTTP header.
/// </summary>
public const string KeepAlive = "Keep-Alive";
/// <summary>
/// The <c>Last-Modified</c> HTTP header.
/// </summary>
public const string LastModified = "Last-Modified";
/// <summary>
/// The <c>Link</c> HTTP header.
/// </summary>
public const string Link = "Link";
/// <summary>
/// The <c>Location</c> HTTP header.
/// </summary>
public const string Location = "Location";
/// <summary>
/// The <c>Max-Forwards</c> HTTP header.
/// </summary>
public const string MaxForwards = "Max-Forwards";
/// <summary>
/// The <c>Origin</c> HTTP header.
/// </summary>
public const string Origin = "Origin";
/// <summary>
/// The <c>P3P</c> HTTP header.
/// </summary>
public const string P3P = "P3P";
/// <summary>
/// The <c>Pragma</c> HTTP header.
/// </summary>
public const string Pragma = "Pragma";
/// <summary>
/// The <c>Proxy-Authenticate</c> HTTP header.
/// </summary>
public const string ProxyAuthenticate = "Proxy-Authenticate";
/// <summary>
/// The <c>Proxy-Authorization</c> HTTP header.
/// </summary>
public const string ProxyAuthorization = "Proxy-Authorization";
/// <summary>
/// The <c>Proxy-Connection</c> HTTP header.
/// </summary>
public const string ProxyConnection = "Proxy-Connection";
/// <summary>
/// The <c>Public-Key-Pins</c> HTTP header.
/// </summary>
public const string PublicKeyPins = "Public-Key-Pins";
/// <summary>
/// The <c>Range</c> HTTP header.
/// </summary>
public const string Range = "Range";
/// <summary>
/// The <c>Referer</c> HTTP header.
/// </summary>
/// <remarks>
/// <para>The incorrect spelling ("Referer" instead of "Referrer") is intentional
/// and has historical reasons.</para>
/// <para>See the "Etymology" section of <a href="https://en.wikipedia.org/wiki/HTTP_referer">the Wikipedia article</a>
/// on this header for more information.</para>
/// </remarks>
public const string Referer = "Referer";
/// <summary>
/// The <c>Retry-After</c> HTTP header.
/// </summary>
public const string RetryAfter = "Retry-After";
/// <summary>
/// The <c>Sec-WebSocket-Accept</c> HTTP header.
/// </summary>
public const string SecWebSocketAccept = "Sec-WebSocket-Accept";
/// <summary>
/// The <c>Sec-WebSocket-Extensions</c> HTTP header.
/// </summary>
public const string SecWebSocketExtensions = "Sec-WebSocket-Extensions";
/// <summary>
/// The <c>Sec-WebSocket-Key</c> HTTP header.
/// </summary>
public const string SecWebSocketKey = "Sec-WebSocket-Key";
/// <summary>
/// The <c>Sec-WebSocket-Protocol</c> HTTP header.
/// </summary>
public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol";
/// <summary>
/// The <c>Sec-WebSocket-Version</c> HTTP header.
/// </summary>
public const string SecWebSocketVersion = "Sec-WebSocket-Version";
/// <summary>
/// The <c>Server</c> HTTP header.
/// </summary>
public const string Server = "Server";
/// <summary>
/// The <c>Set-Cookie</c> HTTP header.
/// </summary>
public const string SetCookie = "Set-Cookie";
/// <summary>
/// The <c>Set-Cookie2</c> HTTP header.
/// </summary>
public const string SetCookie2 = "Set-Cookie2";
/// <summary>
/// The <c>Strict-Transport-Security</c> HTTP header.
/// </summary>
public const string StrictTransportSecurity = "Strict-Transport-Security";
/// <summary>
/// The <c>TE</c> HTTP header.
/// </summary>
public const string TE = "TE";
/// <summary>
/// The <c>TSV</c> HTTP header.
/// </summary>
public const string TSV = "TSV";
/// <summary>
/// The <c>Trailer</c> HTTP header.
/// </summary>
public const string Trailer = "Trailer";
/// <summary>
/// The <c>Transfer-Encoding</c> HTTP header.
/// </summary>
public const string TransferEncoding = "Transfer-Encoding";
/// <summary>
/// The <c>Upgrade</c> HTTP header.
/// </summary>
public const string Upgrade = "Upgrade";
/// <summary>
/// The <c>Upgrade-Insecure-Requests</c> HTTP header.
/// </summary>
public const string UpgradeInsecureRequests = "Upgrade-Insecure-Requests";
/// <summary>
/// The <c>User-Agent</c> HTTP header.
/// </summary>
public const string UserAgent = "User-Agent";
/// <summary>
/// The <c>Vary</c> HTTP header.
/// </summary>
public const string Vary = "Vary";
/// <summary>
/// The <c>Via</c> HTTP header.
/// </summary>
public const string Via = "Via";
/// <summary>
/// The <c>WWW-Authenticate</c> HTTP header.
/// </summary>
public const string WWWAuthenticate = "WWW-Authenticate";
/// <summary>
/// The <c>Warning</c> HTTP header.
/// </summary>
public const string Warning = "Warning";
/// <summary>
/// The <c>X-AspNet-Version</c> HTTP header.
/// </summary>
public const string XAspNetVersion = "X-AspNet-Version";
/// <summary>
/// The <c>X-Content-Duration</c> HTTP header.
/// </summary>
public const string XContentDuration = "X-Content-Duration";
/// <summary>
/// The <c>X-Content-Type-Options</c> HTTP header.
/// </summary>
public const string XContentTypeOptions = "X-Content-Type-Options";
/// <summary>
/// The <c>X-Frame-Options</c> HTTP header.
/// </summary>
public const string XFrameOptions = "X-Frame-Options";
/// <summary>
/// The <c>X-MSEdge-Ref</c> HTTP header.
/// </summary>
public const string XMSEdgeRef = "X-MSEdge-Ref";
/// <summary>
/// The <c>X-Powered-By</c> HTTP header.
/// </summary>
public const string XPoweredBy = "X-Powered-By";
/// <summary>
/// The <c>X-Request-ID</c> HTTP header.
/// </summary>
public const string XRequestID = "X-Request-ID";
/// <summary>
/// The <c>X-UA-Compatible</c> HTTP header.
/// </summary>
public const string XUACompatible = "X-UA-Compatible";
}
}

View File

@@ -0,0 +1,20 @@
namespace EmbedIO
{
/// <summary>
/// Defines the HTTP listeners available for use in a <see cref="WebServer"/>.
/// </summary>
public enum HttpListenerMode
{
/// <summary>
/// Use EmbedIO's internal HTTP listener implementation,
/// based on Mono's <c>System.Net.HttpListener</c>.
/// </summary>
EmbedIO,
/// <summary>
/// Use the <see cref="System.Net.HttpListener"/> class
/// provided by the .NET runtime in use.
/// </summary>
Microsoft,
}
}

View File

@@ -0,0 +1,55 @@
using System.Net;
namespace EmbedIO
{
/// <summary>
/// When thrown, breaks the request handling control flow
/// and sends a redirection response to the client.
/// </summary>
#pragma warning disable CA1032 // Implement standard exception constructors - they have no meaning here.
public class HttpNotAcceptableException : HttpException
#pragma warning restore CA1032
{
/// <summary>
/// Initializes a new instance of the <see cref="HttpNotAcceptableException"/> class,
/// without specifying a value for the response's <c>Vary</c> header.
/// </summary>
public HttpNotAcceptableException()
: this(null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="HttpNotAcceptableException"/> class.
/// </summary>
/// <param name="vary">
/// <para>A value, or a comma-separated list of values, to set the response's <c>Vary</c> header to.</para>
/// <para>Although not specified in <see href="https://tools.ietf.org/html/rfc7231#section-6.5.6">RFC7231</see>,
/// this may help the client to understand why the request has been rejected.</para>
/// <para>If this parameter is <see langword="null"/> or the empty string, the response's <c>Vary</c> header
/// is not set.</para>
/// </param>
public HttpNotAcceptableException(string? vary)
: base((int)HttpStatusCode.NotAcceptable)
{
Vary = string.IsNullOrEmpty(vary) ? null : vary;
}
/// <summary>
/// Gets the value, or comma-separated list of values, to be set
/// on the response's <c>Vary</c> header.
/// </summary>
/// <remarks>
/// <para>If the empty string has been passed to the <see cref="HttpNotAcceptableException(string)"/>
/// constructor, the value of this property is <see langword="null"/>.</para>
/// </remarks>
public string? Vary { get; }
/// <inheritdoc />
public override void PrepareResponse(IHttpContext context)
{
if (Vary != null)
context.Response.Headers.Add(HttpHeaderNames.Vary, Vary);
}
}
}

View File

@@ -0,0 +1,50 @@
using System.Net;
namespace EmbedIO
{
/// <summary>
/// When thrown, breaks the request handling control flow
/// and sends a redirection response to the client.
/// </summary>
#pragma warning disable CA1032 // Implement standard exception constructors - they have no meaning here.
public class HttpRangeNotSatisfiableException : HttpException
#pragma warning restore CA1032
{
/// <summary>
/// Initializes a new instance of the <see cref="HttpRangeNotSatisfiableException"/> class.
/// without specifying a value for the response's <c>Content-Range</c> header.
/// </summary>
public HttpRangeNotSatisfiableException()
: this(null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="HttpRangeNotSatisfiableException"/> class.
/// </summary>
/// <param name="contentLength">The total length of the requested resource, expressed in bytes,
/// or <see langword="null"/> to omit the <c>Content-Range</c> header in the response.</param>
public HttpRangeNotSatisfiableException(long? contentLength)
: base((int)HttpStatusCode.RequestedRangeNotSatisfiable)
{
ContentLength = contentLength;
}
/// <summary>
/// Gets the total content length to be specified
/// on the response's <c>Content-Range</c> header.
/// </summary>
public long? ContentLength { get; }
/// <inheritdoc />
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}");
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Net;
namespace EmbedIO
{
/// <summary>
/// When thrown, breaks the request handling control flow
/// and sends a redirection response to the client.
/// </summary>
#pragma warning disable CA1032 // Implement standard exception constructors - they have no meaning here.
public class HttpRedirectException : HttpException
#pragma warning restore CA1032
{
/// <summary>
/// Initializes a new instance of the <see cref="HttpRedirectException"/> class.
/// </summary>
/// <param name="location">The redirection target.</param>
/// <param name="statusCode">
/// <para>The status code to set on the response, in the range from 300 to 399.</para>
/// <para>By default, status code 302 (<c>Found</c>) is used.</para>
/// </param>
/// <exception cref="ArgumentException"><paramref name="statusCode"/> is not in the 300-399 range.</exception>
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;
}
/// <summary>
/// Initializes a new instance of the <see cref="HttpRedirectException"/> class.
/// </summary>
/// <param name="location">The redirection target.</param>
/// <param name="statusCode">One of the redirection status codes, to be set on the response.</param>
/// <exception cref="ArgumentException"><paramref name="statusCode"/> is not a redirection status code.</exception>
public HttpRedirectException(string location, HttpStatusCode statusCode)
: this(location, (int)statusCode)
{
}
/// <summary>
/// Gets the URL where the client will be redirected.
/// </summary>
public string Location { get; }
/// <inheritdoc />
public override void PrepareResponse(IHttpContext context)
{
context.Redirect(Location, StatusCode);
}
}
}

View File

@@ -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
{
/// <summary>
/// Provides extension methods for types implementing <see cref="IHttpRequest"/>.
/// </summary>
public static class HttpRequestExtensions
{
/// <summary>
/// <para>Returns a string representing the remote IP address and port of an <see cref="IHttpRequest"/> interface.</para>
/// <para>This method can be called even on a <see langword="null"/> interface, or one that has no
/// remote end point, or no remote address; it will always return a non-<see langword="null"/>,
/// non-empty string.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpRequest"/> on which this method is called.</param>
/// <returns>
/// If <paramref name="this"/> is <see langword="null"/>, or its <see cref="IHttpRequest.RemoteEndPoint">RemoteEndPoint</see>
/// is <see langword="null"/>, the string <c>"&lt;null&gt;</c>; otherwise, the remote end point's
/// <see cref="IPEndPoint.Address">Address</see> (or the string <c>"&lt;???&gt;"</c> if it is <see langword="null"/>)
/// followed by a colon and the <see cref="IPEndPoint.Port">Port</see> number.
/// </returns>
public static string SafeGetRemoteEndpointStr(this IHttpRequest @this)
{
var endPoint = @this?.RemoteEndPoint;
return endPoint == null
? "<null>"
: $"{endPoint.Address?.ToString() ?? "<???>"}:{endPoint.Port.ToString(CultureInfo.InvariantCulture)}";
}
/// <summary>
/// <para>Attempts to proactively negotiate a compression method for a response,
/// based on a request's <c>Accept-Encoding</c> header (or lack of it).</para>
/// </summary>
/// <param name="this">The <see cref="IHttpRequest"/> on which this method is called.</param>
/// <param name="preferCompression"><see langword="true"/> if sending compressed data is preferred over
/// sending non-compressed data; otherwise, <see langword="false"/>.</param>
/// <param name="compressionMethod">When this method returns, the compression method to use for the response,
/// if content negotiation is successful. This parameter is passed uninitialized.</param>
/// <param name="prepareResponse">When this method returns, a callback that prepares data in an <see cref="IHttpResponse"/>
/// according to the result of content negotiation. This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if content negotiation is successful;
/// otherwise, <see langword="false"/>.</returns>
/// <remarks>
/// <para>If this method returns <see langword="true"/>, the <paramref name="prepareResponse"/> callback
/// will set appropriate response headers to reflect the results of content negotiation.</para>
/// <para>If this method returns <see langword="false"/>, the <paramref name="prepareResponse"/> callback
/// will throw a <see cref="HttpNotAcceptableException"/> to send a <c>406 Not Acceptable</c> response
/// with the <c>Vary</c> header set to <c>Accept-Encoding</c>,
/// so that the client may know the reason why the request has been rejected.</para>
/// <para>If <paramref name="this"/> has no<c>Accept-Encoding</c> header, this method
/// always returns <see langword="true"/> and sets <paramref name="compressionMethod"/>
/// to <see cref="CompressionMethod.None"/>.</para>
/// </remarks>
/// <seealso cref="HttpNotAcceptableException(string)"/>
public static bool TryNegotiateContentEncoding(
this IHttpRequest @this,
bool preferCompression,
out CompressionMethod compressionMethod,
out Action<IHttpResponse> 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;
}
/// <summary>
/// <para>Checks whether an <c>If-None-Match</c> header exists in a request
/// and, if so, whether it contains a given entity tag.</para>
/// <para>See <see href="https://tools.ietf.org/html/rfc7232#section-3.2">RFC7232, Section 3.2</see>
/// for a normative reference; however, see the Remarks section for more information
/// about the RFC compliance of this method.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpRequest"/> on which this method is called.</param>
/// <param name="entityTag">The entity tag.</param>
/// <param name="headerExists">When this method returns, a value that indicates whether an
/// <c>If-None-Match</c> header is present in <paramref name="this"/>, regardless of the method's
/// return value. This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if an <c>If-None-Match</c> header is present in
/// <paramref name="this"/> and one of the entity tags listed in it is equal to <paramref name="entityTag"/>;
/// <see langword="false"/> otherwise.</returns>
/// <remarks>
/// <para><see href="https://tools.ietf.org/html/rfc7232#section-3.2">RFC7232, Section 3.2</see>
/// states that a weak comparison function (as defined in
/// <see href="https://tools.ietf.org/html/rfc7232#section-2.3.2">RFC7232, Section 2.3.2</see>)
/// must be used for <c>If-None-Match</c>. 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.</para>
/// <para>The behavior of this method is thus not, strictly speaking, RFC7232-compliant;
/// it works, though, with entity tags generated by EmbedIO.</para>
/// </remarks>
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
/// <summary>
/// <para>Checks whether an <c>If-Modified-Since</c> header exists in a request
/// and, if so, whether its value is a date and time more recent or equal to
/// a given <see cref="DateTime"/>.</para>
/// <para>See <see href="https://tools.ietf.org/html/rfc7232#section-3.3">RFC7232, Section 3.3</see>
/// for a normative reference.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpRequest"/> on which this method is called.</param>
/// <param name="lastModifiedUtc">A date and time value, in Coordinated Universal Time,
/// expressing the last time a resource was modified.</param>
/// <param name="headerExists">When this method returns, a value that indicates whether an
/// <c>If-Modified-Since</c> header is present in <paramref name="this"/>, regardless of the method's
/// return value. This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if an <c>If-Modified-Since</c> header is present in
/// <paramref name="this"/> and its value is a date and time more recent or equal to <paramref name="lastModifiedUtc"/>;
/// <see langword="false"/> otherwise.</returns>
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.
/// <summary>
/// <para>Checks whether a <c>Range</c> header exists in a request
/// and, if so, determines whether it is possible to send a <c>206 Partial Content</c> response.</para>
/// <para>See <see href="https://tools.ietf.org/html/rfc7233">RFC7233</see>
/// for a normative reference; however, see the Remarks section for more information
/// about the RFC compliance of this method.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpRequest"/> on which this method is called.</param>
/// <param name="contentLength">The total length, in bytes, of the response entity, i.e.
/// what would be sent in a <c>200 OK</c> response.</param>
/// <param name="entityTag">An entity tag representing the response entity. This value is checked against
/// the <c>If-Range</c> header, if it is present.</param>
/// <param name="lastModifiedUtc">The date and time value, in Coordinated Universal Time,
/// expressing the last modification time of the resource entity. This value is checked against
/// the <c>If-Range</c> header, if it is present.</param>
/// <param name="start">When this method returns <see langword="true"/>, the start of the requested byte range.
/// This parameter is passed uninitialized.</param>
/// <param name="upperBound">
/// <para>When this method returns <see langword="true"/>, the upper bound of the requested byte range.
/// This parameter is passed uninitialized.</para>
/// <para>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 <c>bytes=0-99</c> has a start of 0, an upper bound of 99,
/// and a length of 100 bytes.</para>
/// </param>
/// <returns>
/// <para>This method returns <see langword="true"/> if the following conditions are satisfied:</para>
/// <list type="bullet">
/// <item><description>>the request's HTTP method is <c>GET</c>;</description></item>
/// <item><description>>a <c>Range</c> header is present in the request;</description></item>
/// <item><description>>either no <c>If-Range</c> header is present in the request, or it
/// specifies an entity tag equal to <paramref name="entityTag"/>, or a UTC date and time
/// equal to <paramref name="lastModifiedUtc"/>;</description></item>
/// <item><description>>the <c>Range</c> header specifies exactly one range;</description></item>
/// <item><description>>the specified range is entirely contained in the range from 0 to <paramref name="contentLength"/> - 1.</description></item>
/// </list>
/// <para>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 <paramref name="contentLength"/> - 1, this method does not return;
/// it throws a <see cref="HttpRangeNotSatisfiableException"/> instead.</para>
/// <para>If any of the other conditions are not satisfied, this method returns <see langword="false"/>.</para>
/// </returns>
/// <remarks>
/// <para>According to <see href="https://tools.ietf.org/html/rfc7233#section-3.1">RFC7233, Section 3.1</see>,
/// 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 <c>200 OK</c> response with the whole response
/// entity instead of the requested range(s). For this reason, until the generation of
/// <c>multipart/byteranges</c> 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.</para>
/// <para>To make clients aware that range requests are accepted for a resource, every <c>200 OK</c>
/// (or <c>304 Not Modified</c>) response for the same resource should include an <c>Accept-Ranges</c>
/// header with the string <c>bytes</c> as value.</para>
/// </remarks>
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;
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using EmbedIO.Utilities;
namespace EmbedIO
{
/// <summary>
/// Provides extension methods for types implementing <see cref="IHttpResponse"/>.
/// </summary>
public static class HttpResponseExtensions
{
/// <summary>
/// Sets the necessary headers to disable caching of a response on the client side.
/// </summary>
/// <param name="this">The <see cref="IHttpResponse"/> interface on which this method is called.</param>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
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");
}
/// <summary>
/// Prepares a standard response without a body for the specified status code.
/// </summary>
/// <param name="this">The <see cref="IHttpResponse"/> interface on which this method is called.</param>
/// <param name="statusCode">The HTTP status code of the response.</param>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">There is no standard status description for <paramref name="statusCode"/>.</exception>
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;
}
}
}

View File

@@ -0,0 +1,146 @@
using System.Collections.Generic;
using System.Net;
namespace EmbedIO
{
/// <summary>
/// <para>Provides standard HTTP status descriptions.</para>
/// <para>Data contained in this class comes from the following sources:</para>
/// <list type="bullet">
/// <item><description><see href="https://tools.ietf.org/html/rfc7231#section-6">RFC7231 Section 6</see> (HTTP/1.1 Semantics and Content)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc6585">RFC6585</see> (Additional HTTP Status Codes)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc2774#section-7">RFC2774 Section 7</see> (An HTTP Extension Framework)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc7540#section-9.1.2">RFC7540 Section 9.1.2</see> (HTTP/2)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc4918#section-11">RFC4918 Section 11</see> (WebDAV)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc5842#section-7">RFC5842 Section 7</see> (Binding Extensions to WebDAV)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc7538#section-3">RFC7538 Section 3</see> (HTTP Status Code 308)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc3229#section-10.4.1">RFC3229 Section 10.4.1</see> (Delta encoding in HTTP)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc8297#section-2">RFC8297 Section 2</see> (Early Hints)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc7725#section-3">RFC7725 Section 3</see> (HTTP-status-451)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc2295#section-8.1">RFC2295 Section 8.1</see> (Transparent Content Negotiation)</description></item>
/// </list>
/// </summary>
public static class HttpStatusDescription
{
private static readonly IReadOnlyDictionary<int, string> Dictionary = new Dictionary<int, string> {
{ 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" },
};
/// <summary>
/// Attempts to get the standard status description for a <see cref="HttpStatusCode"/>.
/// </summary>
/// <param name="code">The HTTP status code for which the standard description
/// is to be retrieved.</param>
/// <param name="description">When this method returns, the standard HTTP status description
/// for the specified <paramref name="code"/> if it was found, or <see langword="null"/>
/// if it was not found. This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if the specified <paramref name="code"/> was found
/// in the list of HTTP status codes for which the standard description is known;
/// otherwise, <see langword="false"/>.</returns>
/// <seealso cref="TryGet(int,out string)"/>
/// <seealso cref="Get(HttpStatusCode)"/>
public static bool TryGet(HttpStatusCode code, out string description) => Dictionary.TryGetValue((int)code, out description);
/// <summary>
/// Attempts to get the standard status description for a HTTP status code
/// specified as an <see langword="int"/>.
/// </summary>
/// <param name="code">The HTTP status code for which the standard description
/// is to be retrieved.</param>
/// <param name="description">When this method returns, the standard HTTP status description
/// for the specified <paramref name="code"/> if it was found, or <see langword="null"/>
/// if it was not found. This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if the specified <paramref name="code"/> was found
/// in the list of HTTP status codes for which the standard description is known;
/// otherwise, <see langword="false"/>.</returns>
/// <seealso cref="TryGet(HttpStatusCode,out string)"/>
/// <seealso cref="Get(int)"/>
public static bool TryGet(int code, out string description) => Dictionary.TryGetValue(code, out description);
/// <summary>
/// Returns the standard status description for a <see cref="HttpStatusCode"/>.
/// </summary>
/// <param name="code">The HTTP status code for which the standard description
/// is to be retrieved.</param>
/// <returns>The standard HTTP status description for the specified <paramref name="code"/>
/// if it was found, or <see langword="null"/> if it was not found.</returns>
public static string Get(HttpStatusCode code)
{
Dictionary.TryGetValue((int)code, out var description);
return description;
}
/// <summary>
/// Returns the standard status description for a HTTP status code
/// specified as an <see langword="int"/>.
/// </summary>
/// <param name="code">The HTTP status code for which the standard description
/// is to be retrieved.</param>
/// <returns>The standard HTTP status description for the specified <paramref name="code"/>
/// if it was found, or <see langword="null"/> if it was not found.</returns>
public static string Get(int code)
{
Dictionary.TryGetValue(code, out var description);
return description;
}
}
}

48
Vendor/EmbedIO-3.5.2/HttpVerbs.cs vendored Normal file
View File

@@ -0,0 +1,48 @@
namespace EmbedIO
{
/// <summary>
/// Enumerates the different HTTP Verbs.
/// </summary>
public enum HttpVerbs
{
/// <summary>
/// Wildcard Method
/// </summary>
Any,
/// <summary>
/// DELETE Method
/// </summary>
Delete,
/// <summary>
/// GET Method
/// </summary>
Get,
/// <summary>
/// HEAD method
/// </summary>
Head,
/// <summary>
/// OPTIONS method
/// </summary>
Options,
/// <summary>
/// PATCH method
/// </summary>
Patch,
/// <summary>
/// POST method
/// </summary>
Post,
/// <summary>
/// PUT method
/// </summary>
Put,
}
}

View File

@@ -0,0 +1,49 @@
using System.Net;
using System.Collections;
using System.Collections.Generic;
namespace EmbedIO
{
/// <summary>
/// Interface for Cookie Collection.
/// </summary>
/// <seealso cref="ICollection" />
#pragma warning disable CA1010 // Should implement ICollection<Cookie> - not possible when wrapping System.Net.CookieCollection.
public interface ICookieCollection : IEnumerable<Cookie>, ICollection
#pragma warning restore CA1010
{
/// <summary>
/// Gets the <see cref="Cookie"/> with the specified name.
/// </summary>
/// <value>
/// The <see cref="Cookie"/>.
/// </value>
/// <param name="name">The name.</param>
/// <returns>The cookie matching the specified name.</returns>
Cookie? this[string name] { get; }
/// <summary>
/// Determines whether this <see cref="ICookieCollection"/> contains the specified <see cref="Cookie"/>.
/// </summary>
/// <param name="cookie">The cookie to find in the <see cref="ICookieCollection"/>.</param>
/// <returns>
/// <see langword="true"/> if this <see cref="ICookieCollection"/> contains the specified <paramref name="cookie"/>;
/// otherwise, <see langword="false"/>.
/// </returns>
bool Contains(Cookie cookie);
/// <summary>
/// Copies the elements of this <see cref="ICookieCollection"/> to a <see cref="Cookie"/> array
/// starting at the specified index of the target array.
/// </summary>
/// <param name="array">The target <see cref="Cookie"/> array to which the <see cref="ICookieCollection"/> will be copied.</param>
/// <param name="index">The zero-based index in the target <paramref name="array"/> where copying begins.</param>
void CopyTo(Cookie[] array, int index);
/// <summary>
/// Adds the specified cookie.
/// </summary>
/// <param name="cookie">The cookie.</param>
void Add(Cookie cookie);
}
}

128
Vendor/EmbedIO-3.5.2/IHttpContext.cs vendored Normal file
View File

@@ -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
{
/// <summary>
/// Represents the context of a HTTP(s) request being handled by a web server.
/// </summary>
public interface IHttpContext : IMimeTypeProvider
{
/// <summary>
/// Gets a unique identifier for a HTTP context.
/// </summary>
string Id { get; }
/// <summary>
/// Gets a <see cref="CancellationToken" /> used to stop processing of this context.
/// </summary>
CancellationToken CancellationToken { get; }
/// <summary>
/// Gets the server IP address and port number to which the request is directed.
/// </summary>
IPEndPoint LocalEndPoint { get; }
/// <summary>
/// Gets the client IP address and port number from which the request originated.
/// </summary>
IPEndPoint RemoteEndPoint { get; }
/// <summary>
/// Gets the HTTP request.
/// </summary>
IHttpRequest Request { get; }
/// <summary>
/// Gets the route matched by the requested URL path.
/// </summary>
RouteMatch Route { get; }
/// <summary>
/// Gets the requested path, relative to the innermost module's base path.
/// </summary>
/// <remarks>
/// <para>This property derives from the path specified in the requested URL, stripped of the
/// <see cref="IWebModule.BaseRoute">BaseRoute</see> of the handling module.</para>
/// <para>This property is in itself a valid URL path, including an initial
/// slash (<c>/</c>) character.</para>
/// </remarks>
string RequestedPath { get; }
/// <summary>
/// Gets the HTTP response object.
/// </summary>
IHttpResponse Response { get; }
/// <summary>
/// Gets the user.
/// </summary>
IPrincipal User { get; }
/// <summary>
/// Gets the session proxy associated with this context.
/// </summary>
ISessionProxy Session { get; }
/// <summary>
/// Gets a value indicating whether compressed request bodies are supported.
/// </summary>
/// <seealso cref="WebServerOptionsBase.SupportCompressedRequests"/>
bool SupportCompressedRequests { get; }
/// <summary>
/// Gets the dictionary of data to pass trough the EmbedIO pipeline.
/// </summary>
IDictionary<object, object> Items { get; }
/// <summary>
/// Gets the elapsed time, expressed in milliseconds, since the creation of this context.
/// </summary>
long Age { get; }
/// <summary>
/// <para>Gets a value indicating whether this <see cref="IHttpContext"/>
/// has been completely handled, so that no further processing is required.</para>
/// <para>When a HTTP context is created, this property is <see langword="false" />;
/// as soon as it is set to <see langword="true" />, the context is not
/// passed to any further module's handler for processing.</para>
/// <para>Once it becomes <see langword="true" />, this property is guaranteed
/// to never become <see langword="false" /> again.</para>
/// </summary>
/// <remarks>
/// <para>When a module's <see cref="IWebModule.IsFinalHandler">IsFinalHandler</see> property is
/// <see langword="true" />, this property is set to <see langword="true" /> after the <see cref="Task" />
/// returned by the module's <see cref="IWebModule.HandleRequestAsync">HandleRequestAsync</see> method
/// is completed.</para>
/// </remarks>
/// <seealso cref="SetHandled" />
/// <seealso cref="IWebModule.IsFinalHandler"/>
bool IsHandled { get; }
/// <summary>
/// <para>Marks this context as handled, so that it will not be
/// processed by any further module.</para>
/// </summary>
/// <remarks>
/// <para>Calling this method from the <see cref="IWebModule.HandleRequestAsync" />
/// or <see cref="WebModuleBase.OnRequestAsync" /> of a module whose
/// <see cref="IWebModule.IsFinalHandler" /> property is <see langword="true" />
/// is redundant and has no effect.</para>
/// </remarks>
/// <seealso cref="IsHandled"/>
/// <seealso cref="IWebModule.IsFinalHandler"/>
void SetHandled();
/// <summary>
/// Registers a callback to be called when processing is finished on a context.
/// </summary>
/// <param name="callback">The callback.</param>
void OnClose(Action<IHttpContext> callback);
}
}

View File

@@ -0,0 +1,20 @@
using System.Threading.Tasks;
namespace EmbedIO
{
/// <summary>
/// <para>Represents an object that can handle a HTTP context.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
public interface IHttpContextHandler
{
/// <summary>
/// <para>Asynchronously handles a HTTP context, generating a suitable response
/// for an incoming request.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
Task HandleContextAsync(IHttpContextImpl context);
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using EmbedIO.Routing;
using EmbedIO.Sessions;
using EmbedIO.Utilities;
using EmbedIO.WebSockets;
namespace EmbedIO
{
/// <summary>
/// <para>Represents a HTTP context implementation, i.e. a HTTP context as seen internally by EmbedIO.</para>
/// <para>This API mainly supports the EmbedIO infrastructure; it is not intended to be used directly from your code,
/// unless to address specific needs in the implementation of EmbedIO plug-ins (e.g. modules).</para>
/// </summary>
/// <seealso cref="IHttpContext" />
public interface IHttpContextImpl : IHttpContext
{
/// <summary>
/// <para>Gets or sets a <see cref="CancellationToken" /> used to stop processing of this context.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
new CancellationToken CancellationToken { get; set; }
/// <summary>
/// Gets or sets the route matched by the requested URL path.
/// </summary>
new RouteMatch Route { get; set; }
/// <summary>
/// <para>Gets or sets the session proxy associated with this context.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
/// <value>
/// An <see cref="ISessionProxy"/> interface.
/// </value>
new ISessionProxy Session { get; set; }
/// <summary>
/// <para>Gets or sets the user.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
new IPrincipal User { get; set; }
/// <summary>
/// <para>Gets or sets a value indicating whether compressed request bodies are supported.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
/// <seealso cref="WebServerOptionsBase.SupportCompressedRequests"/>
new bool SupportCompressedRequests { get; set; }
/// <summary>
/// <para>Gets the MIME type providers.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
MimeTypeProviderStack MimeTypeProviders { get; }
/// <summary>
/// <para>Flushes and closes the response stream, then calls any registered close callbacks.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
/// <seealso cref="IHttpContext.OnClose"/>
void Close();
/// <summary>
/// <para>Asynchronously handles a WebSockets opening handshake
/// and returns a newly-created <seealso cref="IWebSocketContext"/> interface.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
/// <param name="requestedProtocols">The requested WebSocket sub-protocols.</param>
/// <param name="acceptedProtocol">The accepted WebSocket sub-protocol,
/// or the empty string is no sub-protocol has been agreed upon.</param>
/// <param name="receiveBufferSize">Size of the receive buffer.</param>
/// <param name="keepAliveInterval">The keep-alive interval.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to stop the server.</param>
/// <returns>
/// An <see cref="IWebSocketContext"/> interface.
/// </returns>
Task<IWebSocketContext> AcceptWebSocketAsync(
IEnumerable<string> requestedProtocols,
string acceptedProtocol,
int receiveBufferSize,
TimeSpan keepAliveInterval,
CancellationToken cancellationToken);
}
}

58
Vendor/EmbedIO-3.5.2/IHttpException.cs vendored Normal file
View File

@@ -0,0 +1,58 @@
using System;
namespace EmbedIO
{
/// <summary>
/// <para>Represents an exception that results in a particular
/// HTTP response to be sent to the client.</para>
/// <para>This interface is meant to be implemented
/// by classes derived from <see cref="Exception" />.</para>
/// <para>Either as message or a data object can be attached to
/// the exception; which one, if any, is sent to the client
/// will depend upon the handler used to send the response.</para>
/// </summary>
/// <seealso cref="HttpExceptionHandlerCallback"/>
/// <seealso cref="HttpExceptionHandler"/>
public interface IHttpException
{
/// <summary>
/// Gets the response status code for a HTTP exception.
/// </summary>
int StatusCode { get; }
/// <summary>
/// Gets the stack trace of a HTTP exception.
/// </summary>
string StackTrace { get; }
/// <summary>
/// <para>Gets a message that can be included in the response triggered
/// by a HTTP exception.</para>
/// <para>Whether the message is actually sent to the client will depend
/// upon the handler used to send the response.</para>
/// </summary>
/// <remarks>
/// <para>Do not rely on <see cref="Exception.Message"/> to implement
/// this property if you want to support <see langword="null"/> messages,
/// because a default message will be supplied by the CLR at throw time
/// when <see cref="Exception.Message"/> is <see langword="null"/>.</para>
/// </remarks>
string? Message { get; }
/// <summary>
/// <para>Gets an object that can be serialized and included
/// in the response triggered by a HTTP exception.</para>
/// <para>Whether the object is actually sent to the client will depend
/// upon the handler used to send the response.</para>
/// </summary>
object? DataObject { get; }
/// <summary>
/// Sets necessary headers, as required by the nature
/// of the HTTP exception (e.g. <c>Location</c> for
/// <see cref="HttpRedirectException" />).
/// </summary>
/// <param name="context">The HTTP context of the response.</param>
void PrepareResponse(IHttpContext context);
}
}

72
Vendor/EmbedIO-3.5.2/IHttpListener.cs vendored Normal file
View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace EmbedIO
{
/// <summary>
/// Interface to create a HTTP Listener.
/// </summary>
public interface IHttpListener : IDisposable
{
/// <summary>
/// Gets or sets a value indicating whether the listener should ignore write exceptions. By default the flag is set on.
/// </summary>
/// <value>
/// <c>true</c> if [ignore write exceptions]; otherwise, <c>false</c>.
/// </value>
bool IgnoreWriteExceptions { get; set; }
/// <summary>
/// Gets the prefixes.
/// </summary>
/// <value>
/// The prefixes.
/// </value>
List<string> Prefixes { get; }
/// <summary>
/// Gets a value indicating whether this instance is listening.
/// </summary>
/// <value>
/// <c>true</c> if this instance is listening; otherwise, <c>false</c>.
/// </value>
bool IsListening { get; }
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>
/// The name.
/// </value>
string Name { get; }
/// <summary>
/// Starts this listener.
/// </summary>
void Start();
/// <summary>
/// Stops this listener.
/// </summary>
#pragma warning disable CA1716 // Rename method to avoid conflict with (VB) keyword - It is consistent with Microsoft's HttpListener
void Stop();
#pragma warning restore CA1716
/// <summary>
/// Adds the prefix.
/// </summary>
/// <param name="urlPrefix">The URL prefix.</param>
void AddPrefix(string urlPrefix);
/// <summary>
/// Gets the HTTP context asynchronous.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// A task that represents the time delay for the HTTP Context.
/// </returns>
Task<IHttpContextImpl> GetContextAsync(CancellationToken cancellationToken);
}
}

26
Vendor/EmbedIO-3.5.2/IHttpMessage.cs vendored Normal file
View File

@@ -0,0 +1,26 @@
using System;
namespace EmbedIO
{
/// <summary>
/// Represents a HTTP request or response.
/// </summary>
public interface IHttpMessage
{
/// <summary>
/// Gets the cookies.
/// </summary>
/// <value>
/// The cookies.
/// </value>
ICookieCollection Cookies { get; }
/// <summary>
/// Gets or sets the protocol version.
/// </summary>
/// <value>
/// The protocol version.
/// </value>
Version ProtocolVersion { get; }
}
}

115
Vendor/EmbedIO-3.5.2/IHttpRequest.cs vendored Normal file
View File

@@ -0,0 +1,115 @@
using System;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Text;
namespace EmbedIO
{
/// <inheritdoc />
/// <summary>
/// Interface to create a HTTP Request.
/// </summary>
public interface IHttpRequest : IHttpMessage
{
/// <summary>
/// Gets the request headers.
/// </summary>
NameValueCollection Headers { get; }
/// <summary>
/// Gets a value indicating whether [keep alive].
/// </summary>
bool KeepAlive { get; }
/// <summary>
/// Gets the raw URL.
/// </summary>
string RawUrl { get; }
/// <summary>
/// Gets the query string.
/// </summary>
NameValueCollection QueryString { get; }
/// <summary>
/// Gets the HTTP method.
/// </summary>
string HttpMethod { get; }
/// <summary>
/// Gets a <see cref="HttpVerbs"/> constant representing the HTTP method of the request.
/// </summary>
HttpVerbs HttpVerb { get; }
/// <summary>
/// Gets the URL.
/// </summary>
Uri Url { get; }
/// <summary>
/// Gets a value indicating whether this instance has entity body.
/// </summary>
bool HasEntityBody { get; }
/// <summary>
/// Gets the input stream.
/// </summary>
Stream InputStream { get; }
/// <summary>
/// Gets the content encoding.
/// </summary>
Encoding ContentEncoding { get; }
/// <summary>
/// Gets the remote end point.
/// </summary>
IPEndPoint RemoteEndPoint { get; }
/// <summary>
/// Gets a value indicating whether this instance is local.
/// </summary>
bool IsLocal { get; }
/// <summary>
/// Gets a value indicating whether this request has been received over a SSL connection.
/// </summary>
bool IsSecureConnection { get; }
/// <summary>
/// Gets the user agent.
/// </summary>
string UserAgent { get; }
/// <summary>
/// Gets a value indicating whether this instance is web socket request.
/// </summary>
bool IsWebSocketRequest { get; }
/// <summary>
/// Gets the local end point.
/// </summary>
IPEndPoint LocalEndPoint { get; }
/// <summary>
/// Gets the type of the content.
/// </summary>
string? ContentType { get; }
/// <summary>
/// Gets the content length.
/// </summary>
long ContentLength64 { get; }
/// <summary>
/// Gets a value indicating whether this instance is authenticated.
/// </summary>
bool IsAuthenticated { get; }
/// <summary>
/// Gets the URL referrer.
/// </summary>
Uri? UrlReferrer { get; }
}
}

69
Vendor/EmbedIO-3.5.2/IHttpResponse.cs vendored Normal file
View File

@@ -0,0 +1,69 @@
using System.IO;
using System.Net;
using System.Text;
namespace EmbedIO
{
/// <inheritdoc />
/// <summary>
/// Interface to create a HTTP Response.
/// </summary>
public interface IHttpResponse : IHttpMessage
{
/// <summary>
/// Gets the response headers.
/// </summary>
WebHeaderCollection Headers { get; }
/// <summary>
/// Gets or sets the status code.
/// </summary>
int StatusCode { get; set; }
/// <summary>
/// Gets or sets the content length.
/// </summary>
long ContentLength64 { get; set; }
/// <summary>
/// Gets or sets the type of the content.
/// </summary>
string ContentType { get; set; }
/// <summary>
/// Gets the output stream.
/// </summary>
Stream OutputStream { get; }
/// <summary>
/// Gets or sets the content encoding.
/// </summary>
Encoding? ContentEncoding { get; set; }
/// <summary>
/// Gets or sets a value indicating whether [keep alive].
/// </summary>
bool KeepAlive { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the response uses chunked transfer encoding.
/// </summary>
bool SendChunked { get; set; }
/// <summary>
/// Gets or sets a text description of the HTTP status code.
/// </summary>
string StatusDescription { get; set; }
/// <summary>
/// Sets the cookie.
/// </summary>
/// <param name="cookie">The session cookie.</param>
void SetCookie(Cookie cookie);
/// <summary>
/// Closes this instance and dispose the resources.
/// </summary>
void Close();
}
}

View File

@@ -0,0 +1,45 @@
using System;
namespace EmbedIO
{
/// <summary>
/// Represents an object that can set information about specific MIME types and media ranges,
/// to be later retrieved via an <see cref="IMimeTypeProvider"/> interface.
/// </summary>
/// <seealso cref="IMimeTypeProvider" />
public interface IMimeTypeCustomizer : IMimeTypeProvider
{
/// <summary>
/// Adds a custom association between a file extension and a MIME type.
/// </summary>
/// <param name="extension">The file extension to associate to <paramref name="mimeType"/>.</param>
/// <param name="mimeType">The MIME type to associate to <paramref name="extension"/>.</param>
/// <exception cref="InvalidOperationException">The object implementing <see cref="IMimeTypeCustomizer"/>
/// has its configuration locked.</exception>
/// <exception cref="ArgumentNullException">
/// <para><paramref name="extension"/>is <see langword="null"/>.</para>
/// <para>- or -</para>
/// <para><paramref name="mimeType"/>is <see langword="null"/>.</para>
/// </exception>
/// <exception cref="ArgumentException">
/// <para><paramref name="extension"/>is the empty string.</para>
/// <para>- or -</para>
/// <para><paramref name="mimeType"/>is not a valid MIME type.</para>
/// </exception>
void AddCustomMimeType(string extension, string mimeType);
/// <summary>
/// Indicates whether to prefer compression when negotiating content encoding
/// for a response with the specified content type, or whose content type is in
/// the specified media range.
/// </summary>
/// <param name="mimeType">The MIME type or media range.</param>
/// <param name="preferCompression"><see langword="true"/> to prefer compression;
/// otherwise, <see langword="false"/>.</param>
/// <exception cref="InvalidOperationException">The object implementing <see cref="IMimeTypeCustomizer"/>
/// has its configuration locked.</exception>
/// <exception cref="ArgumentNullException"><paramref name="mimeType"/>is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="mimeType"/>is not a valid MIME type or media range.</exception>
void PreferCompression(string mimeType, bool preferCompression);
}
}

View File

@@ -0,0 +1,31 @@
using System;
namespace EmbedIO
{
/// <summary>
/// Represents an object that contains information on specific MIME types and media ranges.
/// </summary>
public interface IMimeTypeProvider
{
/// <summary>
/// Gets the MIME type associated to a file extension.
/// </summary>
/// <param name="extension">The file extension for which a corresponding MIME type is wanted.</param>
/// <returns>The MIME type corresponding to <paramref name="extension"/>, if one is found;
/// otherwise, <see langword="null"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="extension"/>is <see langword="null"/>.</exception>
string GetMimeType(string extension);
/// <summary>
/// Attempts to determine whether compression should be preferred
/// when negotiating content encoding for a response with the specified content type.
/// </summary>
/// <param name="mimeType">The MIME type to check.</param>
/// <param name="preferCompression">When this method returns <see langword="true"/>,
/// a value indicating whether compression should be preferred.
/// This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if a value is found for <paramref name="mimeType"/>;
/// otherwise, <see langword="false"/>.</returns>
bool TryDetermineCompression(string mimeType, out bool preferCompression);
}
}

81
Vendor/EmbedIO-3.5.2/IWebModule.cs vendored Normal file
View File

@@ -0,0 +1,81 @@
using System.Threading;
using System.Threading.Tasks;
using EmbedIO.Routing;
namespace EmbedIO
{
/// <summary>
/// Represents a module.
/// </summary>
public interface IWebModule
{
/// <summary>
/// Gets the base route of a module.
/// </summary>
/// <value>
/// The base route.
/// </value>
/// <remarks>
/// <para>A base route is either "/" (the root path),
/// or a prefix starting and ending with a '/' character.</para>
/// </remarks>
string BaseRoute { get; }
/// <summary>
/// Gets a value indicating whether processing of a request should stop
/// after a module has handled it.
/// </summary>
/// <remarks>
/// <para>If this property is <see langword="true" />, a HTTP context's
/// <see cref="IHttpContext.SetHandled" /> method will be automatically called
/// immediately after after the <see cref="Task" /> returned by
/// <see cref="HandleRequestAsync" /> is completed. This will prevent
/// the context from being passed further along to other modules.</para>
/// </remarks>
/// <seealso cref="IHttpContext.IsHandled" />
/// <seealso cref="IHttpContext.SetHandled" />
bool IsFinalHandler { get; }
/// <summary>
/// <para>Gets or sets a callback that is called every time an unhandled exception
/// occurs during the processing of a request.</para>
/// <para>If this property is <see langword="null"/> (the default),
/// the exception will be handled by the web server, or by the containing
/// <see cref="ModuleGroup"/>.</para>
/// </summary>
/// <seealso cref="ExceptionHandler"/>
ExceptionHandlerCallback? OnUnhandledException { get; set; }
/// <summary>
/// <para>Gets or sets a callback that is called every time a HTTP exception
/// is thrown during the processing of a request.</para>
/// <para>If this property is <see langword="null"/> (the default),
/// the exception will be handled by the web server, or by the containing
/// <see cref="ModuleGroup"/>.</para>
/// </summary>
/// <seealso cref="HttpExceptionHandler"/>
HttpExceptionHandlerCallback? OnHttpException { get; set; }
/// <summary>
/// Signals a module that the web server is starting.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to stop the web server.</param>
void Start(CancellationToken cancellationToken);
/// <summary>
/// Matches the specified URL path against a module's <see cref="BaseRoute"/>,
/// extracting values for the route's parameters and a sub-path.
/// </summary>
/// <param name="urlPath">The URL path to match.</param>
/// <returns>If the match is successful, a <see cref="RouteMatch"/> object;
/// otherwise, <see langword="null"/>.</returns>
RouteMatch MatchUrlPath(string urlPath);
/// <summary>
/// Handles a request from a client.
/// </summary>
/// <param name="context">The context of the request being handled.</param>
/// <returns>A <see cref="Task" /> representing the ongoing operation.</returns>
Task HandleRequestAsync(IHttpContext context);
}
}

View File

@@ -0,0 +1,19 @@
using System;
using EmbedIO.Utilities;
namespace EmbedIO
{
/// <summary>
/// Represents an object that contains a collection of <see cref="IWebModule"/> interfaces.
/// </summary>
public interface IWebModuleContainer : IDisposable
{
/// <summary>
/// Gets the modules.
/// </summary>
/// <value>
/// The modules.
/// </value>
IComponentCollection<IWebModule> Modules { get; }
}
}

68
Vendor/EmbedIO-3.5.2/IWebServer.cs vendored Normal file
View File

@@ -0,0 +1,68 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using EmbedIO.Sessions;
namespace EmbedIO
{
/// <summary>
/// <para>Represents a web server.</para>
/// <para>The basic usage of a web server is as follows:</para>
/// <list type="bullet">
/// <item><description>add modules to the <see cref="IWebModuleContainer.Modules">Modules</see> collection;</description></item>
/// <item><description>set a <see cref="SessionManager"/> if needed;</description></item>
/// <item><description>call <see cref="RunAsync"/> to respond to incoming requests.</description></item>
/// </list>
/// </summary>
public interface IWebServer : IWebModuleContainer, IMimeTypeCustomizer
{
/// <summary>
/// Occurs when the <see cref="State"/> property changes.
/// </summary>
event WebServerStateChangedEventHandler StateChanged;
/// <summary>
/// <para>Gets or sets a callback that is called every time an unhandled exception
/// occurs during the processing of a request.</para>
/// <para>This property can never be <see langword="null"/>.
/// If it is still </para>
/// </summary>
/// <seealso cref="ExceptionHandler"/>
ExceptionHandlerCallback OnUnhandledException { get; set; }
/// <summary>
/// <para>Gets or sets a callback that is called every time a HTTP exception
/// is thrown during the processing of a request.</para>
/// <para>This property can never be <see langword="null"/>.</para>
/// </summary>
/// <seealso cref="HttpExceptionHandler"/>
HttpExceptionHandlerCallback OnHttpException { get; set; }
/// <summary>
/// <para>Gets or sets the registered session ID manager, if any.</para>
/// <para>A session ID manager is an implementation of <see cref="ISessionManager"/>.</para>
/// <para>Note that this property can only be set before starting the web server.</para>
/// </summary>
/// <value>
/// The session manager, or <see langword="null"/> if no session manager is present.
/// </value>
/// <exception cref="InvalidOperationException">This property is being set and the web server has already been started.</exception>
ISessionManager? SessionManager { get; set; }
/// <summary>
/// Gets the state of the web server.
/// </summary>
/// <value>The state.</value>
/// <seealso cref="WebServerState"/>
WebServerState State { get; }
/// <summary>
/// Starts the listener and the registered modules.
/// </summary>
/// <param name="cancellationToken">The cancellation token; when cancelled, the server cancels all pending requests and stops.</param>
/// <returns>
/// Returns the task that the HTTP listener is running inside of, so that it can be waited upon after it's been canceled.
/// </returns>
Task RunAsync(CancellationToken cancellationToken = default);
}
}

View File

@@ -0,0 +1,87 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace EmbedIO.Internal
{
// Wraps a response's output stream, buffering all data
// in a MemoryStream.
// When disposed, sets the response's ContentLength and copies all data
// to the output stream.
internal class BufferingResponseStream : Stream
{
private readonly IHttpResponse _response;
private readonly MemoryStream _buffer;
public BufferingResponseStream(IHttpResponse response)
{
_response = response;
_buffer = new MemoryStream();
}
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length => _buffer.Length;
public override long Position
{
get => _buffer.Position;
set => throw SeekingNotSupported();
}
public override void Flush() => _buffer.Flush();
public override Task FlushAsync(CancellationToken cancellationToken) => _buffer.FlushAsync(cancellationToken);
public override int Read(byte[] buffer, int offset, int count) => throw ReadingNotSupported();
public override int ReadByte() => throw ReadingNotSupported();
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
=> throw ReadingNotSupported();
public override int EndRead(IAsyncResult asyncResult) => throw ReadingNotSupported();
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw ReadingNotSupported();
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
=> throw ReadingNotSupported();
public override long Seek(long offset, SeekOrigin origin) => throw SeekingNotSupported();
public override void SetLength(long value) => throw SeekingNotSupported();
public override void Write(byte[] buffer, int offset, int count) => _buffer.Write(buffer, offset, count);
public override void WriteByte(byte value) => _buffer.WriteByte(value);
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
=> _buffer.BeginWrite(buffer, offset, count, callback, state);
public override void EndWrite(IAsyncResult asyncResult) => _buffer.EndWrite(asyncResult);
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> _buffer.WriteAsync(buffer, offset, count, cancellationToken);
protected override void Dispose(bool disposing)
{
_response.ContentLength64 = _buffer.Length;
_buffer.Position = 0;
_buffer.CopyTo(_response.OutputStream);
if (disposing)
{
_buffer.Dispose();
}
}
private static Exception ReadingNotSupported() => new NotSupportedException("This stream does not support reading.");
private static Exception SeekingNotSupported() => new NotSupportedException("This stream does not support seeking.");
}
}

View File

@@ -0,0 +1,121 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Threading;
using System.Threading.Tasks;
namespace EmbedIO.Internal
{
internal class CompressionStream : Stream
{
private readonly Stream _target;
private readonly bool _leaveOpen;
public CompressionStream(Stream target, CompressionMethod compressionMethod)
{
switch (compressionMethod)
{
case CompressionMethod.Deflate:
_target = new DeflateStream(target, CompressionMode.Compress, true);
_leaveOpen = false;
break;
case CompressionMethod.Gzip:
_target = new GZipStream(target, CompressionMode.Compress, true);
_leaveOpen = false;
break;
default:
_target = target;
_leaveOpen = true;
break;
}
UncompressedLength = 0;
}
public long UncompressedLength { get; private set; }
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length => throw SeekingNotSupported();
public override long Position
{
get => throw SeekingNotSupported();
set => throw SeekingNotSupported();
}
public override void Flush() => _target.Flush();
public override Task FlushAsync(CancellationToken cancellationToken) => _target.FlushAsync(cancellationToken);
public override int Read(byte[] buffer, int offset, int count) => throw ReadingNotSupported();
public override int ReadByte() => throw ReadingNotSupported();
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
=> throw ReadingNotSupported();
public override int EndRead(IAsyncResult asyncResult) => throw ReadingNotSupported();
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw ReadingNotSupported();
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
=> throw ReadingNotSupported();
public override long Seek(long offset, SeekOrigin origin) => throw SeekingNotSupported();
public override void SetLength(long value) => throw SeekingNotSupported();
public override void Write(byte[] buffer, int offset, int count)
{
_target.Write(buffer, offset, count);
UncompressedLength += count;
}
public override void WriteByte(byte value)
{
_target.WriteByte(value);
UncompressedLength++;
}
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
=> _target.BeginWrite(
buffer,
offset,
count,
ar => {
UncompressedLength += count;
callback(ar);
},
state);
public override void EndWrite(IAsyncResult asyncResult)
{
_target.EndWrite(asyncResult);
}
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
await _target.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
UncompressedLength += count;
}
protected override void Dispose(bool disposing)
{
if (disposing && !_leaveOpen)
{
_target.Dispose();
}
base.Dispose(disposing);
}
private static Exception ReadingNotSupported() => new NotSupportedException("This stream does not support reading.");
private static Exception SeekingNotSupported() => new NotSupportedException("This stream does not support seeking.");
}
}

View File

@@ -0,0 +1,82 @@
using System.IO;
using System.IO.Compression;
namespace EmbedIO.Internal
{
internal static class CompressionUtility
{
public static byte[]? ConvertCompression(byte[] source, CompressionMethod sourceMethod, CompressionMethod targetMethod)
{
if (source == null)
return null;
if (sourceMethod == targetMethod)
return source;
switch (sourceMethod)
{
case CompressionMethod.Deflate:
using (var sourceStream = new MemoryStream(source, false))
{
using var decompressionStream = new DeflateStream(sourceStream, CompressionMode.Decompress, true);
using var targetStream = new MemoryStream();
if (targetMethod == CompressionMethod.Gzip)
{
using var compressionStream = new GZipStream(targetStream, CompressionMode.Compress, true);
decompressionStream.CopyTo(compressionStream);
}
else
{
decompressionStream.CopyTo(targetStream);
}
return targetStream.ToArray();
}
case CompressionMethod.Gzip:
using (var sourceStream = new MemoryStream(source, false))
{
using var decompressionStream = new GZipStream(sourceStream, CompressionMode.Decompress, true);
using var targetStream = new MemoryStream();
if (targetMethod == CompressionMethod.Deflate)
{
using var compressionStream = new DeflateStream(targetStream, CompressionMode.Compress, true);
decompressionStream.CopyToAsync(compressionStream);
}
else
{
decompressionStream.CopyTo(targetStream);
}
return targetStream.ToArray();
}
default:
using (var sourceStream = new MemoryStream(source, false))
{
using var targetStream = new MemoryStream();
switch (targetMethod)
{
case CompressionMethod.Deflate:
using (var compressionStream = new DeflateStream(targetStream, CompressionMode.Compress, true))
sourceStream.CopyTo(compressionStream);
break;
case CompressionMethod.Gzip:
using (var compressionStream = new GZipStream(targetStream, CompressionMode.Compress, true))
sourceStream.CopyTo(compressionStream);
break;
default:
// Just in case. Consider all other values as None.
return source;
}
return targetStream.ToArray();
}
}
}
}
}

View File

@@ -0,0 +1,27 @@
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using EmbedIO.Utilities;
using Swan;
namespace EmbedIO.Internal
{
internal sealed class DummyWebModuleContainer : IWebModuleContainer
{
public static readonly IWebModuleContainer Instance = new DummyWebModuleContainer();
private DummyWebModuleContainer()
{
}
public IComponentCollection<IWebModule> Modules => throw UnexpectedCall();
public ConcurrentDictionary<object, object> SharedItems => throw UnexpectedCall();
public void Dispose()
{
}
private InternalErrorException UnexpectedCall([CallerMemberName] string member = "")
=> SelfCheck.Failure($"Unexpected call to {nameof(DummyWebModuleContainer)}.{member}.");
}
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Specialized;
namespace EmbedIO.Internal
{
internal sealed class LockableNameValueCollection : NameValueCollection
{
public void MakeReadOnly() => IsReadOnly = true;
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using EmbedIO.Utilities;
using Swan.Configuration;
namespace EmbedIO.Internal
{
internal sealed class MimeTypeCustomizer : ConfiguredObject, IMimeTypeCustomizer
{
private readonly Dictionary<string, string> _customMimeTypes = new Dictionary<string, string>();
private readonly Dictionary<(string, string), bool> _data = new Dictionary<(string, string), bool>();
private bool? _defaultPreferCompression;
public string GetMimeType(string extension)
{
_customMimeTypes.TryGetValue(Validate.NotNull(nameof(extension), extension), out var result);
return result;
}
public bool TryDetermineCompression(string mimeType, out bool preferCompression)
{
var (type, subtype) = MimeType.UnsafeSplit(
Validate.MimeType(nameof(mimeType), mimeType, false));
if (_data.TryGetValue((type, subtype), out preferCompression))
return true;
if (_data.TryGetValue((type, "*"), out preferCompression))
return true;
if (!_defaultPreferCompression.HasValue)
return false;
preferCompression = _defaultPreferCompression.Value;
return true;
}
public void AddCustomMimeType(string extension, string mimeType)
{
EnsureConfigurationNotLocked();
_customMimeTypes[Validate.NotNullOrEmpty(nameof(extension), extension)]
= Validate.MimeType(nameof(mimeType), mimeType, false);
}
public void PreferCompression(string mimeType, bool preferCompression)
{
EnsureConfigurationNotLocked();
var (type, subtype) = MimeType.UnsafeSplit(
Validate.MimeType(nameof(mimeType), mimeType, true));
if (type == "*")
{
_defaultPreferCompression = preferCompression;
}
else
{
_data[(type, subtype)] = preferCompression;
}
}
public void Lock() => LockConfiguration();
}
}

View File

@@ -0,0 +1,15 @@
using System;
namespace EmbedIO.Internal
{
// This exception is only created and handled internally,
// so it doesn't need all the standard the bells and whistles.
#pragma warning disable CA1032 // Add standard exception constructors
#pragma warning disable CA1064 // Exceptions should be public
internal class RequestHandlerPassThroughException : Exception
{
}
#pragma warning restore CA1032
#pragma warning restore CA1064
}

View File

@@ -0,0 +1,27 @@
using System.Diagnostics;
namespace EmbedIO.Internal
{
/// <summary>
/// Represents a wrapper around Stopwatch.
/// </summary>
public sealed class TimeKeeper
{
private static readonly Stopwatch Stopwatch = Stopwatch.StartNew();
private readonly long _start;
/// <summary>
/// Initializes a new instance of the <see cref="TimeKeeper"/> class.
/// </summary>
public TimeKeeper()
{
_start = Stopwatch.ElapsedMilliseconds;
}
/// <summary>
/// Gets the elapsed time since the class was initialized.
/// </summary>
public long ElapsedTime => Stopwatch.ElapsedMilliseconds - _start;
}
}

View File

@@ -0,0 +1,47 @@
using System;
namespace EmbedIO.Internal
{
internal static class UriUtility
{
public static Uri StringToUri(string str)
{
_ = Uri.TryCreate(str, CanBeAbsoluteUrl(str) ? UriKind.Absolute : UriKind.Relative, out var result);
return result;
}
public static Uri? StringToAbsoluteUri(string str)
{
if (!CanBeAbsoluteUrl(str))
{
return null;
}
_ = Uri.TryCreate(str, UriKind.Absolute, out var result);
return result;
}
// Returns true if string starts with "http:", "https:", "ws:", or "wss:"
private static bool CanBeAbsoluteUrl(string str)
=> !string.IsNullOrEmpty(str)
&& str[0] switch {
'h' => str.Length >= 5
&& str[1] == 't'
&& str[2] == 't'
&& str[3] == 'p'
&& str[4] switch {
':' => true,
's' => str.Length >= 6 && str[5] == ':',
_ => false
},
'w' => str.Length >= 3
&& str[1] == 's'
&& str[2] switch {
':' => true,
's' => str.Length >= 4 && str[3] == ':',
_ => false
},
_ => false
};
}
}

View File

@@ -0,0 +1,46 @@
using System.Threading;
using System.Threading.Tasks;
using EmbedIO.Utilities;
using Swan.Logging;
namespace EmbedIO.Internal
{
internal sealed class WebModuleCollection : DisposableComponentCollection<IWebModule>
{
private readonly string _logSource;
internal WebModuleCollection(string logSource)
{
_logSource = logSource;
}
internal void StartAll(CancellationToken cancellationToken)
{
foreach (var (name, module) in WithSafeNames)
{
$"Starting module {name}...".Debug(_logSource);
module.Start(cancellationToken);
}
}
internal async Task DispatchRequestAsync(IHttpContext context)
{
if (context.IsHandled)
return;
var requestedPath = context.RequestedPath;
foreach (var (name, module) in WithSafeNames)
{
var routeMatch = module.MatchUrlPath(requestedPath);
if (routeMatch == null)
continue;
$"[{context.Id}] Processing with {name}.".Debug(_logSource);
context.GetImplementation().Route = routeMatch;
await module.HandleRequestAsync(context).ConfigureAwait(false);
if (context.IsHandled)
break;
}
}
}
}

View File

@@ -0,0 +1,648 @@
using System;
using System.Collections.Generic;
namespace EmbedIO
{
partial class MimeType
{
// -------------------------------------------------------------------------------------------------
//
// IMPORTANT NOTE TO CONTRIBUTORS
// ==============================
//
// When you update the MIME type list, remember to:
//
// * update the date in XML docs below;
//
// * check the LICENSE file to see if copyright year and/or license conditions have changed;
//
// * if the URL for the LICENSE file has changed, update EmbedIO's LICENSE file too.
//
// -------------------------------------------------------------------------------------------------
/// <summary>
/// <para>Associates file extensions to MIME types.</para>
/// </summary>
/// <remarks>
/// <para>The list of MIME types has been copied from
/// <see href="https://github.com/samuelneff/MimeTypeMap/blob/master/src/MimeTypes/MimeTypeMap.cs">Samuel Neff's MIME Type Map</see>
/// on April 26th, 2019.</para>
/// <para>Copyright (c) 2014 Samuel Neff. Redistributed under <see href="https://github.com/samuelneff/MimeTypeMap/blob/master/LICENSE">MIT license</see>.</para>
/// </remarks>
public static IReadOnlyDictionary<string, string> Associations { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{".323", "text/h323"},
{".3g2", "video/3gpp2"},
{".3gp", "video/3gpp"},
{".3gp2", "video/3gpp2"},
{".3gpp", "video/3gpp"},
{".7z", "application/x-7z-compressed"},
{".aa", "audio/audible"},
{".AAC", "audio/aac"},
{".aaf", "application/octet-stream"},
{".aax", "audio/vnd.audible.aax"},
{".ac3", "audio/ac3"},
{".aca", "application/octet-stream"},
{".accda", "application/msaccess.addin"},
{".accdb", "application/msaccess"},
{".accdc", "application/msaccess.cab"},
{".accde", "application/msaccess"},
{".accdr", "application/msaccess.runtime"},
{".accdt", "application/msaccess"},
{".accdw", "application/msaccess.webapplication"},
{".accft", "application/msaccess.ftemplate"},
{".acx", "application/internet-property-stream"},
{".AddIn", "text/xml"},
{".ade", "application/msaccess"},
{".adobebridge", "application/x-bridge-url"},
{".adp", "application/msaccess"},
{".ADT", "audio/vnd.dlna.adts"},
{".ADTS", "audio/aac"},
{".afm", "application/octet-stream"},
{".ai", "application/postscript"},
{".aif", "audio/aiff"},
{".aifc", "audio/aiff"},
{".aiff", "audio/aiff"},
{".air", "application/vnd.adobe.air-application-installer-package+zip"},
{".amc", "application/mpeg"},
{".anx", "application/annodex"},
{".apk", "application/vnd.android.package-archive" },
{".application", "application/x-ms-application"},
{".art", "image/x-jg"},
{".asa", "application/xml"},
{".asax", "application/xml"},
{".ascx", "application/xml"},
{".asd", "application/octet-stream"},
{".asf", "video/x-ms-asf"},
{".ashx", "application/xml"},
{".asi", "application/octet-stream"},
{".asm", "text/plain"},
{".asmx", "application/xml"},
{".aspx", "application/xml"},
{".asr", "video/x-ms-asf"},
{".asx", "video/x-ms-asf"},
{".atom", "application/atom+xml"},
{".au", "audio/basic"},
{".avi", "video/x-msvideo"},
{".axa", "audio/annodex"},
{".axs", "application/olescript"},
{".axv", "video/annodex"},
{".bas", "text/plain"},
{".bcpio", "application/x-bcpio"},
{".bin", "application/octet-stream"},
{".bmp", "image/bmp"},
{".c", "text/plain"},
{".cab", "application/octet-stream"},
{".caf", "audio/x-caf"},
{".calx", "application/vnd.ms-office.calx"},
{".cat", "application/vnd.ms-pki.seccat"},
{".cc", "text/plain"},
{".cd", "text/plain"},
{".cdda", "audio/aiff"},
{".cdf", "application/x-cdf"},
{".cer", "application/x-x509-ca-cert"},
{".cfg", "text/plain"},
{".chm", "application/octet-stream"},
{".class", "application/x-java-applet"},
{".clp", "application/x-msclip"},
{".cmd", "text/plain"},
{".cmx", "image/x-cmx"},
{".cnf", "text/plain"},
{".cod", "image/cis-cod"},
{".config", "application/xml"},
{".contact", "text/x-ms-contact"},
{".coverage", "application/xml"},
{".cpio", "application/x-cpio"},
{".cpp", "text/plain"},
{".crd", "application/x-mscardfile"},
{".crl", "application/pkix-crl"},
{".crt", "application/x-x509-ca-cert"},
{".cs", "text/plain"},
{".csdproj", "text/plain"},
{".csh", "application/x-csh"},
{".csproj", "text/plain"},
{".css", "text/css"},
{".csv", "text/csv"},
{".cur", "application/octet-stream"},
{".cxx", "text/plain"},
{".dat", "application/octet-stream"},
{".datasource", "application/xml"},
{".dbproj", "text/plain"},
{".dcr", "application/x-director"},
{".def", "text/plain"},
{".deploy", "application/octet-stream"},
{".der", "application/x-x509-ca-cert"},
{".dgml", "application/xml"},
{".dib", "image/bmp"},
{".dif", "video/x-dv"},
{".dir", "application/x-director"},
{".disco", "text/xml"},
{".divx", "video/divx"},
{".dll", "application/x-msdownload"},
{".dll.config", "text/xml"},
{".dlm", "text/dlm"},
{".doc", "application/msword"},
{".docm", "application/vnd.ms-word.document.macroEnabled.12"},
{".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
{".dot", "application/msword"},
{".dotm", "application/vnd.ms-word.template.macroEnabled.12"},
{".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"},
{".dsp", "application/octet-stream"},
{".dsw", "text/plain"},
{".dtd", "text/xml"},
{".dtsConfig", "text/xml"},
{".dv", "video/x-dv"},
{".dvi", "application/x-dvi"},
{".dwf", "drawing/x-dwf"},
{".dwg", "application/acad"},
{".dwp", "application/octet-stream"},
{".dxf", "application/x-dxf" },
{".dxr", "application/x-director"},
{".eml", "message/rfc822"},
{".emz", "application/octet-stream"},
{".eot", "application/vnd.ms-fontobject"},
{".eps", "application/postscript"},
{".es", "application/ecmascript"},
{".etl", "application/etl"},
{".etx", "text/x-setext"},
{".evy", "application/envoy"},
{".exe", "application/octet-stream"},
{".exe.config", "text/xml"},
{".fdf", "application/vnd.fdf"},
{".fif", "application/fractals"},
{".filters", "application/xml"},
{".fla", "application/octet-stream"},
{".flac", "audio/flac"},
{".flr", "x-world/x-vrml"},
{".flv", "video/x-flv"},
{".fsscript", "application/fsharp-script"},
{".fsx", "application/fsharp-script"},
{".generictest", "application/xml"},
{".gif", "image/gif"},
{".gpx", "application/gpx+xml"},
{".group", "text/x-ms-group"},
{".gsm", "audio/x-gsm"},
{".gtar", "application/x-gtar"},
{".gz", "application/x-gzip"},
{".h", "text/plain"},
{".hdf", "application/x-hdf"},
{".hdml", "text/x-hdml"},
{".hhc", "application/x-oleobject"},
{".hhk", "application/octet-stream"},
{".hhp", "application/octet-stream"},
{".hlp", "application/winhlp"},
{".hpp", "text/plain"},
{".hqx", "application/mac-binhex40"},
{".hta", "application/hta"},
{".htc", "text/x-component"},
{".htm", "text/html"},
{".html", "text/html"},
{".htt", "text/webviewhtml"},
{".hxa", "application/xml"},
{".hxc", "application/xml"},
{".hxd", "application/octet-stream"},
{".hxe", "application/xml"},
{".hxf", "application/xml"},
{".hxh", "application/octet-stream"},
{".hxi", "application/octet-stream"},
{".hxk", "application/xml"},
{".hxq", "application/octet-stream"},
{".hxr", "application/octet-stream"},
{".hxs", "application/octet-stream"},
{".hxt", "text/html"},
{".hxv", "application/xml"},
{".hxw", "application/octet-stream"},
{".hxx", "text/plain"},
{".i", "text/plain"},
{".ico", "image/x-icon"},
{".ics", "application/octet-stream"},
{".idl", "text/plain"},
{".ief", "image/ief"},
{".iii", "application/x-iphone"},
{".inc", "text/plain"},
{".inf", "application/octet-stream"},
{".ini", "text/plain"},
{".inl", "text/plain"},
{".ins", "application/x-internet-signup"},
{".ipa", "application/x-itunes-ipa"},
{".ipg", "application/x-itunes-ipg"},
{".ipproj", "text/plain"},
{".ipsw", "application/x-itunes-ipsw"},
{".iqy", "text/x-ms-iqy"},
{".isp", "application/x-internet-signup"},
{".ite", "application/x-itunes-ite"},
{".itlp", "application/x-itunes-itlp"},
{".itms", "application/x-itunes-itms"},
{".itpc", "application/x-itunes-itpc"},
{".IVF", "video/x-ivf"},
{".jar", "application/java-archive"},
{".java", "application/octet-stream"},
{".jck", "application/liquidmotion"},
{".jcz", "application/liquidmotion"},
{".jfif", "image/pjpeg"},
{".jnlp", "application/x-java-jnlp-file"},
{".jpb", "application/octet-stream"},
{".jpe", "image/jpeg"},
{".jpeg", "image/jpeg"},
{".jpg", "image/jpeg"},
{".js", "application/javascript"},
{".json", "application/json"},
{".jsx", "text/jscript"},
{".jsxbin", "text/plain"},
{".latex", "application/x-latex"},
{".library-ms", "application/windows-library+xml"},
{".lit", "application/x-ms-reader"},
{".loadtest", "application/xml"},
{".lpk", "application/octet-stream"},
{".lsf", "video/x-la-asf"},
{".lst", "text/plain"},
{".lsx", "video/x-la-asf"},
{".lzh", "application/octet-stream"},
{".m13", "application/x-msmediaview"},
{".m14", "application/x-msmediaview"},
{".m1v", "video/mpeg"},
{".m2t", "video/vnd.dlna.mpeg-tts"},
{".m2ts", "video/vnd.dlna.mpeg-tts"},
{".m2v", "video/mpeg"},
{".m3u", "audio/x-mpegurl"},
{".m3u8", "audio/x-mpegurl"},
{".m4a", "audio/m4a"},
{".m4b", "audio/m4b"},
{".m4p", "audio/m4p"},
{".m4r", "audio/x-m4r"},
{".m4v", "video/x-m4v"},
{".mac", "image/x-macpaint"},
{".mak", "text/plain"},
{".man", "application/x-troff-man"},
{".manifest", "application/x-ms-manifest"},
{".map", "text/plain"},
{".master", "application/xml"},
{".mbox", "application/mbox"},
{".mda", "application/msaccess"},
{".mdb", "application/x-msaccess"},
{".mde", "application/msaccess"},
{".mdp", "application/octet-stream"},
{".me", "application/x-troff-me"},
{".mfp", "application/x-shockwave-flash"},
{".mht", "message/rfc822"},
{".mhtml", "message/rfc822"},
{".mid", "audio/mid"},
{".midi", "audio/mid"},
{".mix", "application/octet-stream"},
{".mk", "text/plain"},
{".mk3d", "video/x-matroska-3d"},
{".mka", "audio/x-matroska"},
{".mkv", "video/x-matroska"},
{".mmf", "application/x-smaf"},
{".mno", "text/xml"},
{".mny", "application/x-msmoney"},
{".mod", "video/mpeg"},
{".mov", "video/quicktime"},
{".movie", "video/x-sgi-movie"},
{".mp2", "video/mpeg"},
{".mp2v", "video/mpeg"},
{".mp3", "audio/mpeg"},
{".mp4", "video/mp4"},
{".mp4v", "video/mp4"},
{".mpa", "video/mpeg"},
{".mpe", "video/mpeg"},
{".mpeg", "video/mpeg"},
{".mpf", "application/vnd.ms-mediapackage"},
{".mpg", "video/mpeg"},
{".mpp", "application/vnd.ms-project"},
{".mpv2", "video/mpeg"},
{".mqv", "video/quicktime"},
{".ms", "application/x-troff-ms"},
{".msg", "application/vnd.ms-outlook"},
{".msi", "application/octet-stream"},
{".mso", "application/octet-stream"},
{".mts", "video/vnd.dlna.mpeg-tts"},
{".mtx", "application/xml"},
{".mvb", "application/x-msmediaview"},
{".mvc", "application/x-miva-compiled"},
{".mxp", "application/x-mmxp"},
{".nc", "application/x-netcdf"},
{".nsc", "video/x-ms-asf"},
{".nws", "message/rfc822"},
{".ocx", "application/octet-stream"},
{".oda", "application/oda"},
{".odb", "application/vnd.oasis.opendocument.database"},
{".odc", "application/vnd.oasis.opendocument.chart"},
{".odf", "application/vnd.oasis.opendocument.formula"},
{".odg", "application/vnd.oasis.opendocument.graphics"},
{".odh", "text/plain"},
{".odi", "application/vnd.oasis.opendocument.image"},
{".odl", "text/plain"},
{".odm", "application/vnd.oasis.opendocument.text-master"},
{".odp", "application/vnd.oasis.opendocument.presentation"},
{".ods", "application/vnd.oasis.opendocument.spreadsheet"},
{".odt", "application/vnd.oasis.opendocument.text"},
{".oga", "audio/ogg"},
{".ogg", "audio/ogg"},
{".ogv", "video/ogg"},
{".ogx", "application/ogg"},
{".one", "application/onenote"},
{".onea", "application/onenote"},
{".onepkg", "application/onenote"},
{".onetmp", "application/onenote"},
{".onetoc", "application/onenote"},
{".onetoc2", "application/onenote"},
{".opus", "audio/ogg"},
{".orderedtest", "application/xml"},
{".osdx", "application/opensearchdescription+xml"},
{".otf", "application/font-sfnt"},
{".otg", "application/vnd.oasis.opendocument.graphics-template"},
{".oth", "application/vnd.oasis.opendocument.text-web"},
{".otp", "application/vnd.oasis.opendocument.presentation-template"},
{".ots", "application/vnd.oasis.opendocument.spreadsheet-template"},
{".ott", "application/vnd.oasis.opendocument.text-template"},
{".oxt", "application/vnd.openofficeorg.extension"},
{".p10", "application/pkcs10"},
{".p12", "application/x-pkcs12"},
{".p7b", "application/x-pkcs7-certificates"},
{".p7c", "application/pkcs7-mime"},
{".p7m", "application/pkcs7-mime"},
{".p7r", "application/x-pkcs7-certreqresp"},
{".p7s", "application/pkcs7-signature"},
{".pbm", "image/x-portable-bitmap"},
{".pcast", "application/x-podcast"},
{".pct", "image/pict"},
{".pcx", "application/octet-stream"},
{".pcz", "application/octet-stream"},
{".pdf", "application/pdf"},
{".pfb", "application/octet-stream"},
{".pfm", "application/octet-stream"},
{".pfx", "application/x-pkcs12"},
{".pgm", "image/x-portable-graymap"},
{".pic", "image/pict"},
{".pict", "image/pict"},
{".pkgdef", "text/plain"},
{".pkgundef", "text/plain"},
{".pko", "application/vnd.ms-pki.pko"},
{".pls", "audio/scpls"},
{".pma", "application/x-perfmon"},
{".pmc", "application/x-perfmon"},
{".pml", "application/x-perfmon"},
{".pmr", "application/x-perfmon"},
{".pmw", "application/x-perfmon"},
{".png", "image/png"},
{".pnm", "image/x-portable-anymap"},
{".pnt", "image/x-macpaint"},
{".pntg", "image/x-macpaint"},
{".pnz", "image/png"},
{".pot", "application/vnd.ms-powerpoint"},
{".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12"},
{".potx", "application/vnd.openxmlformats-officedocument.presentationml.template"},
{".ppa", "application/vnd.ms-powerpoint"},
{".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12"},
{".ppm", "image/x-portable-pixmap"},
{".pps", "application/vnd.ms-powerpoint"},
{".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"},
{".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"},
{".ppt", "application/vnd.ms-powerpoint"},
{".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12"},
{".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
{".prf", "application/pics-rules"},
{".prm", "application/octet-stream"},
{".prx", "application/octet-stream"},
{".ps", "application/postscript"},
{".psc1", "application/PowerShell"},
{".psd", "application/octet-stream"},
{".psess", "application/xml"},
{".psm", "application/octet-stream"},
{".psp", "application/octet-stream"},
{".pst", "application/vnd.ms-outlook"},
{".pub", "application/x-mspublisher"},
{".pwz", "application/vnd.ms-powerpoint"},
{".qht", "text/x-html-insertion"},
{".qhtm", "text/x-html-insertion"},
{".qt", "video/quicktime"},
{".qti", "image/x-quicktime"},
{".qtif", "image/x-quicktime"},
{".qtl", "application/x-quicktimeplayer"},
{".qxd", "application/octet-stream"},
{".ra", "audio/x-pn-realaudio"},
{".ram", "audio/x-pn-realaudio"},
{".rar", "application/x-rar-compressed"},
{".ras", "image/x-cmu-raster"},
{".rat", "application/rat-file"},
{".rc", "text/plain"},
{".rc2", "text/plain"},
{".rct", "text/plain"},
{".rdlc", "application/xml"},
{".reg", "text/plain"},
{".resx", "application/xml"},
{".rf", "image/vnd.rn-realflash"},
{".rgb", "image/x-rgb"},
{".rgs", "text/plain"},
{".rm", "application/vnd.rn-realmedia"},
{".rmi", "audio/mid"},
{".rmp", "application/vnd.rn-rn_music_package"},
{".roff", "application/x-troff"},
{".rpm", "audio/x-pn-realaudio-plugin"},
{".rqy", "text/x-ms-rqy"},
{".rtf", "application/rtf"},
{".rtx", "text/richtext"},
{".rvt", "application/octet-stream" },
{".ruleset", "application/xml"},
{".s", "text/plain"},
{".safariextz", "application/x-safari-safariextz"},
{".scd", "application/x-msschedule"},
{".scr", "text/plain"},
{".sct", "text/scriptlet"},
{".sd2", "audio/x-sd2"},
{".sdp", "application/sdp"},
{".sea", "application/octet-stream"},
{".searchConnector-ms", "application/windows-search-connector+xml"},
{".setpay", "application/set-payment-initiation"},
{".setreg", "application/set-registration-initiation"},
{".settings", "application/xml"},
{".sgimb", "application/x-sgimb"},
{".sgml", "text/sgml"},
{".sh", "application/x-sh"},
{".shar", "application/x-shar"},
{".shtml", "text/html"},
{".sit", "application/x-stuffit"},
{".sitemap", "application/xml"},
{".skin", "application/xml"},
{".skp", "application/x-koan" },
{".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12"},
{".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"},
{".slk", "application/vnd.ms-excel"},
{".sln", "text/plain"},
{".slupkg-ms", "application/x-ms-license"},
{".smd", "audio/x-smd"},
{".smi", "application/octet-stream"},
{".smx", "audio/x-smd"},
{".smz", "audio/x-smd"},
{".snd", "audio/basic"},
{".snippet", "application/xml"},
{".snp", "application/octet-stream"},
{".sol", "text/plain"},
{".sor", "text/plain"},
{".spc", "application/x-pkcs7-certificates"},
{".spl", "application/futuresplash"},
{".spx", "audio/ogg"},
{".src", "application/x-wais-source"},
{".srf", "text/plain"},
{".SSISDeploymentManifest", "text/xml"},
{".ssm", "application/streamingmedia"},
{".sst", "application/vnd.ms-pki.certstore"},
{".stl", "application/vnd.ms-pki.stl"},
{".sv4cpio", "application/x-sv4cpio"},
{".sv4crc", "application/x-sv4crc"},
{".svc", "application/xml"},
{".svg", "image/svg+xml"},
{".swf", "application/x-shockwave-flash"},
{".step", "application/step"},
{".stp", "application/step"},
{".t", "application/x-troff"},
{".tar", "application/x-tar"},
{".tcl", "application/x-tcl"},
{".testrunconfig", "application/xml"},
{".testsettings", "application/xml"},
{".tex", "application/x-tex"},
{".texi", "application/x-texinfo"},
{".texinfo", "application/x-texinfo"},
{".tgz", "application/x-compressed"},
{".thmx", "application/vnd.ms-officetheme"},
{".thn", "application/octet-stream"},
{".tif", "image/tiff"},
{".tiff", "image/tiff"},
{".tlh", "text/plain"},
{".tli", "text/plain"},
{".toc", "application/octet-stream"},
{".tr", "application/x-troff"},
{".trm", "application/x-msterminal"},
{".trx", "application/xml"},
{".ts", "video/vnd.dlna.mpeg-tts"},
{".tsv", "text/tab-separated-values"},
{".ttf", "application/font-sfnt"},
{".tts", "video/vnd.dlna.mpeg-tts"},
{".txt", "text/plain"},
{".u32", "application/octet-stream"},
{".uls", "text/iuls"},
{".user", "text/plain"},
{".ustar", "application/x-ustar"},
{".vb", "text/plain"},
{".vbdproj", "text/plain"},
{".vbk", "video/mpeg"},
{".vbproj", "text/plain"},
{".vbs", "text/vbscript"},
{".vcf", "text/x-vcard"},
{".vcproj", "application/xml"},
{".vcs", "text/plain"},
{".vcxproj", "application/xml"},
{".vddproj", "text/plain"},
{".vdp", "text/plain"},
{".vdproj", "text/plain"},
{".vdx", "application/vnd.ms-visio.viewer"},
{".vml", "text/xml"},
{".vscontent", "application/xml"},
{".vsct", "text/xml"},
{".vsd", "application/vnd.visio"},
{".vsi", "application/ms-vsi"},
{".vsix", "application/vsix"},
{".vsixlangpack", "text/xml"},
{".vsixmanifest", "text/xml"},
{".vsmdi", "application/xml"},
{".vspscc", "text/plain"},
{".vss", "application/vnd.visio"},
{".vsscc", "text/plain"},
{".vssettings", "text/xml"},
{".vssscc", "text/plain"},
{".vst", "application/vnd.visio"},
{".vstemplate", "text/xml"},
{".vsto", "application/x-ms-vsto"},
{".vsw", "application/vnd.visio"},
{".vsx", "application/vnd.visio"},
{".vtt", "text/vtt"},
{".vtx", "application/vnd.visio"},
{".wasm", "application/wasm"},
{".wav", "audio/wav"},
{".wave", "audio/wav"},
{".wax", "audio/x-ms-wax"},
{".wbk", "application/msword"},
{".wbmp", "image/vnd.wap.wbmp"},
{".wcm", "application/vnd.ms-works"},
{".wdb", "application/vnd.ms-works"},
{".wdp", "image/vnd.ms-photo"},
{".webarchive", "application/x-safari-webarchive"},
{".webm", "video/webm"},
{".webp", "image/webp"}, /* https://en.wikipedia.org/wiki/WebP */
{".webtest", "application/xml"},
{".wiq", "application/xml"},
{".wiz", "application/msword"},
{".wks", "application/vnd.ms-works"},
{".WLMP", "application/wlmoviemaker"},
{".wlpginstall", "application/x-wlpg-detect"},
{".wlpginstall3", "application/x-wlpg3-detect"},
{".wm", "video/x-ms-wm"},
{".wma", "audio/x-ms-wma"},
{".wmd", "application/x-ms-wmd"},
{".wmf", "application/x-msmetafile"},
{".wml", "text/vnd.wap.wml"},
{".wmlc", "application/vnd.wap.wmlc"},
{".wmls", "text/vnd.wap.wmlscript"},
{".wmlsc", "application/vnd.wap.wmlscriptc"},
{".wmp", "video/x-ms-wmp"},
{".wmv", "video/x-ms-wmv"},
{".wmx", "video/x-ms-wmx"},
{".wmz", "application/x-ms-wmz"},
{".woff", "application/font-woff"},
{".woff2", "application/font-woff2"},
{".wpl", "application/vnd.ms-wpl"},
{".wps", "application/vnd.ms-works"},
{".wri", "application/x-mswrite"},
{".wrl", "x-world/x-vrml"},
{".wrz", "x-world/x-vrml"},
{".wsc", "text/scriptlet"},
{".wsdl", "text/xml"},
{".wvx", "video/x-ms-wvx"},
{".x", "application/directx"},
{".xaf", "x-world/x-vrml"},
{".xaml", "application/xaml+xml"},
{".xap", "application/x-silverlight-app"},
{".xbap", "application/x-ms-xbap"},
{".xbm", "image/x-xbitmap"},
{".xdr", "text/plain"},
{".xht", "application/xhtml+xml"},
{".xhtml", "application/xhtml+xml"},
{".xla", "application/vnd.ms-excel"},
{".xlam", "application/vnd.ms-excel.addin.macroEnabled.12"},
{".xlc", "application/vnd.ms-excel"},
{".xld", "application/vnd.ms-excel"},
{".xlk", "application/vnd.ms-excel"},
{".xll", "application/vnd.ms-excel"},
{".xlm", "application/vnd.ms-excel"},
{".xls", "application/vnd.ms-excel"},
{".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12"},
{".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12"},
{".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
{".xlt", "application/vnd.ms-excel"},
{".xltm", "application/vnd.ms-excel.template.macroEnabled.12"},
{".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"},
{".xlw", "application/vnd.ms-excel"},
{".xml", "text/xml"},
{".xmp", "application/octet-stream" },
{".xmta", "application/xml"},
{".xof", "x-world/x-vrml"},
{".XOML", "text/plain"},
{".xpm", "image/x-xpixmap"},
{".xps", "application/vnd.ms-xpsdocument"},
{".xrm-ms", "text/xml"},
{".xsc", "application/xml"},
{".xsd", "text/xml"},
{".xsf", "text/xml"},
{".xsl", "text/xml"},
{".xslt", "text/xml"},
{".xsn", "application/octet-stream"},
{".xss", "application/xml"},
{".xspf", "application/xspf+xml"},
{".xtp", "application/octet-stream"},
{".xwd", "image/x-xwindowdump"},
{".z", "application/x-compress"},
{".zip", "application/zip"},
};
}
}

174
Vendor/EmbedIO-3.5.2/MimeType.cs vendored Normal file
View File

@@ -0,0 +1,174 @@
using System;
using EmbedIO.Utilities;
namespace EmbedIO
{
/// <summary>
/// Provides constants for commonly-used MIME types and association between file extensions and MIME types.
/// </summary>
/// <seealso cref="Associations"/>
public static partial class MimeType
{
/// <summary>
/// The default MIME type for data whose type is unknown,
/// i.e. <c>application/octet-stream</c>.
/// </summary>
public const string Default = "application/octet-stream";
/// <summary>
/// The MIME type for plain text, i.e. <c>text/plain</c>.
/// </summary>
public const string PlainText = "text/plain";
/// <summary>
/// The MIME type for HTML, i.e. <c>text/html</c>.
/// </summary>
public const string Html = "text/html";
/// <summary>
/// The MIME type for JSON, i.e. <c>application/json</c>.
/// </summary>
public const string Json = "application/json";
/// <summary>
/// The MIME type for URL-encoded HTML forms,
/// i.e. <c>application/x-www-form-urlencoded</c>.
/// </summary>
internal const string UrlEncodedForm = "application/x-www-form-urlencoded";
/// <summary>
/// <para>Strips parameters, if present (e.g. <c>; encoding=UTF-8</c>), from a MIME type.</para>
/// </summary>
/// <param name="value">The MIME type.</param>
/// <returns><paramref name="value"/> without parameters.</returns>
/// <remarks>
/// <para>This method does not validate <paramref name="value"/>: if it is not
/// a valid MIME type or media range, it is just returned unchanged.</para>
/// </remarks>
public static string StripParameters(string value)
{
if (string.IsNullOrEmpty(value))
return value;
var semicolonPos = value.IndexOf(';');
return semicolonPos < 0
? value
: value.Substring(0, semicolonPos).TrimEnd();
}
/// <summary>
/// Determines whether the specified string is a valid MIME type or media range.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="acceptMediaRange">If set to <see langword="true"/>, both media ranges
/// (e.g. <c>"text/*"</c>, <c>"*/*"</c>) and specific MIME types (e.g. <c>"text/html"</c>)
/// are considered valid; if set to <see langword="false"/>, only specific MIME types
/// are considered valid.</param>
/// <returns><see langword="true"/> if <paramref name="value"/> is valid,
/// according to the value of <paramref name="acceptMediaRange"/>;
/// otherwise, <see langword="false"/>.</returns>
public static bool IsMimeType(string value, bool acceptMediaRange)
{
if (string.IsNullOrEmpty(value))
return false;
var slashPos = value.IndexOf('/');
if (slashPos < 0)
return false;
var isWildcardSubtype = false;
var subtype = value.Substring(slashPos + 1);
if (subtype == "*")
{
if (!acceptMediaRange)
return false;
isWildcardSubtype = true;
}
else if (!Validate.IsRfc2616Token(subtype))
{
return false;
}
var type = value.Substring(0, slashPos);
return type == "*"
? acceptMediaRange && isWildcardSubtype
: Validate.IsRfc2616Token(type);
}
/// <summary>
/// Splits the specified MIME type or media range into type and subtype.
/// </summary>
/// <param name="mimeType">The MIME type or media range to split.</param>
/// <returns>A tuple of type and subtype.</returns>
/// <exception cref="ArgumentNullException"><paramref name="mimeType"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="mimeType"/> is not a valid
/// MIME type or media range.</exception>
public static (string type, string subtype) Split(string mimeType)
=> UnsafeSplit(Validate.MimeType(nameof(mimeType), mimeType, true));
/// <summary>
/// Matches the specified MIME type to a media range.
/// </summary>
/// <param name="mimeType">The MIME type to match.</param>
/// <param name="mediaRange">The media range.</param>
/// <returns><see langword="true"/> if <paramref name="mediaRange"/> is either
/// the same as <paramref name="mimeType"/>, or has the same type and a subtype
/// of <c>"*"</c>, or is <c>"*/*"</c>.</returns>
/// <exception cref="ArgumentNullException">
/// <para><paramref name="mimeType"/> is <see langword="null"/>.</para>
/// <para>- or -</para>
/// <para><paramref name="mediaRange"/> is <see langword="null"/>.</para>
/// </exception>
/// <exception cref="ArgumentException">
/// <para><paramref name="mimeType"/> is not a valid MIME type.</para>
/// <para>- or -</para>
/// <para><paramref name="mediaRange"/> is not a valid MIME media range.</para>
/// </exception>
public static bool IsInRange(string mimeType, string mediaRange)
=> UnsafeIsInRange(
Validate.MimeType(nameof(mimeType), mimeType, false),
Validate.MimeType(nameof(mediaRange), mediaRange, true));
internal static (string type, string subtype) UnsafeSplit(string mimeType)
{
var slashPos = mimeType.IndexOf('/');
return (mimeType.Substring(0, slashPos), mimeType.Substring(slashPos + 1));
}
internal static bool UnsafeIsInRange(string mimeType, string mediaRange)
{
// A validated media range that starts with '*' can only be '*/*'
if (mediaRange[0] == '*')
return true;
var typeSlashPos = mimeType.IndexOf('/');
var rangeSlashPos = mediaRange.IndexOf('/');
if (typeSlashPos != rangeSlashPos)
return false;
for (var i = 0; i < typeSlashPos; i++)
{
if (mimeType[i] != mediaRange[i])
return false;
}
// A validated token has at least 1 character,
// thus there must be at least 1 character after a slash.
if (mediaRange[rangeSlashPos + 1] == '*')
return true;
if (mimeType.Length != mediaRange.Length)
return false;
for (var i = typeSlashPos + 1; i < mimeType.Length; i++)
{
if (mimeType[i] != mediaRange[i])
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,99 @@
using System;
namespace EmbedIO
{
/// <summary>
/// Provides extension methods for types implementing <see cref="IMimeTypeCustomizer"/>.
/// </summary>
public static class MimeTypeCustomizerExtensions
{
/// <summary>
/// Adds a custom association between a file extension and a MIME type.
/// </summary>
/// <typeparam name="T">The type of the object to which this method is applied.</typeparam>
/// <param name="this">The object to which this method is applied.</param>
/// <param name="extension">The file extension to associate to <paramref name="mimeType"/>.</param>
/// <param name="mimeType">The MIME type to associate to <paramref name="extension"/>.</param>
/// <returns><paramref name="this"/> with the custom association added.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException"><paramref name="this"/> has its configuration locked.</exception>
/// <exception cref="ArgumentNullException">
/// <para><paramref name="extension"/>is <see langword="null"/>.</para>
/// <para>- or -</para>
/// <para><paramref name="mimeType"/>is <see langword="null"/>.</para>
/// </exception>
/// <exception cref="ArgumentException">
/// <para><paramref name="extension"/>is the empty string.</para>
/// <para>- or -</para>
/// <para><paramref name="mimeType"/>is not a valid MIME type.</para>
/// </exception>
public static T WithCustomMimeType<T>(this T @this, string extension, string mimeType)
where T : IMimeTypeCustomizer
{
@this.AddCustomMimeType(extension, mimeType);
return @this;
}
/// <summary>
/// Indicates whether to prefer compression when negotiating content encoding
/// for a response with the specified content type, or whose content type is in
/// the specified media range.
/// </summary>
/// <typeparam name="T">The type of the object to which this method is applied.</typeparam>
/// <param name="this">The object to which this method is applied.</param>
/// <param name="mimeType">The MIME type or media range.</param>
/// <param name="preferCompression"><see langword="true"/> to prefer compression;
/// otherwise, <see langword="false"/>.</param>
/// <returns><paramref name="this"/> with the specified preference added.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException"><paramref name="this"/> has its configuration locked.</exception>
/// <exception cref="ArgumentNullException"><paramref name="mimeType"/>is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="mimeType"/>is not a valid MIME type or media range.</exception>
public static T PreferCompressionFor<T>(this T @this, string mimeType, bool preferCompression)
where T : IMimeTypeCustomizer
{
@this.PreferCompression(mimeType, preferCompression);
return @this;
}
/// <summary>
/// Indicates that compression should be preferred when negotiating content encoding
/// for a response with the specified content type, or whose content type is in
/// the specified media range.
/// </summary>
/// <typeparam name="T">The type of the object to which this method is applied.</typeparam>
/// <param name="this">The object to which this method is applied.</param>
/// <param name="mimeType">The MIME type or media range.</param>
/// <returns><paramref name="this"/> with the specified preference added.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException"><paramref name="this"/> has its configuration locked.</exception>
/// <exception cref="ArgumentNullException"><paramref name="mimeType"/>is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="mimeType"/>is not a valid MIME type or media range.</exception>
public static T PreferCompressionFor<T>(this T @this, string mimeType)
where T : IMimeTypeCustomizer
{
@this.PreferCompression(mimeType, true);
return @this;
}
/// <summary>
/// Indicates that no compression should be preferred when negotiating content encoding
/// for a response with the specified content type, or whose content type is in
/// the specified media range.
/// </summary>
/// <typeparam name="T">The type of the object to which this method is applied.</typeparam>
/// <param name="this">The object to which this method is applied.</param>
/// <param name="mimeType">The MIME type or media range.</param>
/// <returns><paramref name="this"/> with the specified preference added.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException"><paramref name="this"/> has its configuration locked.</exception>
/// <exception cref="ArgumentNullException"><paramref name="mimeType"/>is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="mimeType"/>is not a valid MIME type or media range.</exception>
public static T PreferNoCompressionFor<T>(this T @this, string mimeType)
where T : IMimeTypeCustomizer
{
@this.PreferCompression(mimeType, false);
return @this;
}
}
}

111
Vendor/EmbedIO-3.5.2/ModuleGroup.cs vendored Normal file
View File

@@ -0,0 +1,111 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EmbedIO.Internal;
using EmbedIO.Utilities;
namespace EmbedIO
{
/// <summary>
/// <para>Groups modules under a common base URL path.</para>
/// <para>The <see cref="IWebModule.BaseRoute">BaseRoute</see> property
/// of modules contained in a <c>ModuleGroup</c> is relative to the
/// <c>ModuleGroup</c>'s <see cref="IWebModule.BaseRoute">BaseRoute</see> property.
/// For example, given the following code:</para>
/// <para><code>new ModuleGroup("/download")
/// .WithStaticFilesAt("/docs", "/var/my/documents");</code></para>
/// <para>files contained in the <c>/var/my/documents</c> folder will be
/// available to clients under the <c>/download/docs/</c> URL.</para>
/// </summary>
/// <seealso cref="WebModuleBase" />
/// <seealso cref="IDisposable" />
/// <seealso cref="IWebModuleContainer" />
public class ModuleGroup : WebModuleBase, IDisposable, IWebModuleContainer, IMimeTypeCustomizer
{
private readonly WebModuleCollection _modules;
private readonly MimeTypeCustomizer _mimeTypeCustomizer = new MimeTypeCustomizer();
/// <summary>
/// Initializes a new instance of the <see cref="ModuleGroup" /> class.
/// </summary>
/// <param name="baseRoute">The base route served by this module.</param>
/// <param name="isFinalHandler">The value to set the <see cref="IWebModule.IsFinalHandler" /> property to.
/// See the help for the property for more information.</param>
/// <seealso cref="IWebModule.BaseRoute" />
/// <seealso cref="IWebModule.IsFinalHandler" />
public ModuleGroup(string baseRoute, bool isFinalHandler)
: base(baseRoute)
{
IsFinalHandler = isFinalHandler;
_modules = new WebModuleCollection(nameof(ModuleGroup));
}
/// <summary>
/// Finalizes an instance of the <see cref="ModuleGroup"/> class.
/// </summary>
~ModuleGroup()
{
Dispose(false);
}
/// <inheritdoc />
public sealed override bool IsFinalHandler { get; }
/// <inheritdoc />
public IComponentCollection<IWebModule> Modules => _modules;
/// <inheritdoc />
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);
/// <inheritdoc />
public void AddCustomMimeType(string extension, string mimeType)
=> _mimeTypeCustomizer.AddCustomMimeType(extension, mimeType);
/// <inheritdoc />
public void PreferCompression(string mimeType, bool preferCompression)
=> _mimeTypeCustomizer.PreferCompression(mimeType, preferCompression);
/// <inheritdoc />
protected override Task OnRequestAsync(IHttpContext context)
=> _modules.DispatchRequestAsync(context);
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources;
/// <see langword="false"/> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!disposing)
return;
_modules.Dispose();
}
/// <inheritdoc />
protected override void OnBeforeLockConfiguration()
{
base.OnBeforeLockConfiguration();
_mimeTypeCustomizer.Lock();
}
/// <inheritdoc />
protected override void OnStart(CancellationToken cancellationToken)
{
_modules.StartAll(cancellationToken);
}
}
}

243
Vendor/EmbedIO-3.5.2/Net/CookieList.cs vendored Normal file
View File

@@ -0,0 +1,243 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using EmbedIO.Internal;
using EmbedIO.Net.Internal;
using EmbedIO.Utilities;
namespace EmbedIO.Net
{
/// <summary>
/// <para>Provides a collection container for instances of <see cref="Cookie"/>.</para>
/// <para>This class is meant to be used internally by EmbedIO; you don't need to
/// use this class directly.</para>
/// </summary>
#pragma warning disable CA1710 // Rename class to end in 'Collection' - it ends in 'List', i.e. 'Indexed Collection'.
public sealed class CookieList : List<Cookie>, ICookieCollection
#pragma warning restore CA1710
{
/// <inheritdoc />
public bool IsSynchronized => false;
/// <inheritdoc />
public Cookie? this[string name]
{
get
{
if (name == null)
throw new ArgumentNullException(nameof(name));
if (Count == 0)
return null;
var list = new List<Cookie>(this);
list.Sort(CompareCookieWithinSorted);
return list.FirstOrDefault(cookie => cookie.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
}
/// <summary>Creates a <see cref="CookieList"/> by parsing
/// the value of one or more <c>Cookie</c> or <c>Set-Cookie</c> headers.</summary>
/// <param name="headerValue">The value, or comma-separated list of values,
/// of the header or headers.</param>
/// <returns>A newly-created instance of <see cref="CookieList"/>.</returns>
public static CookieList Parse(string headerValue)
{
var cookies = new CookieList();
Cookie? cookie = null;
var pairs = SplitCookieHeaderValue(headerValue);
for (var i = 0; i < pairs.Length; i++)
{
var pair = pairs[i].Trim();
if (pair.Length == 0)
continue;
if (pair.StartsWith("version", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.Version = int.Parse(GetValue(pair, true), CultureInfo.InvariantCulture);
}
else if (pair.StartsWith("expires", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
var buff = new StringBuilder(GetValue(pair), 32);
if (i < pairs.Length - 1)
buff.AppendFormat(CultureInfo.InvariantCulture, ", {0}", pairs[++i].Trim());
if (!HttpDate.TryParse(buff.ToString(), out var expires))
expires = DateTimeOffset.Now;
if (cookie.Expires == DateTime.MinValue)
cookie.Expires = expires.LocalDateTime;
}
else if (pair.StartsWith("max-age", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
var max = int.Parse(GetValue(pair, true), CultureInfo.InvariantCulture);
cookie.Expires = DateTime.Now.AddSeconds(max);
}
else if (pair.StartsWith("path", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.Path = GetValue(pair);
}
else if (pair.StartsWith("domain", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.Domain = GetValue(pair);
}
else if (pair.StartsWith("port", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.Port = pair.Equals("port", StringComparison.OrdinalIgnoreCase)
? "\"\""
: GetValue(pair);
}
else if (pair.StartsWith("comment", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.Comment = WebUtility.UrlDecode(GetValue(pair));
}
else if (pair.StartsWith("commenturl", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.CommentUri = UriUtility.StringToUri(GetValue(pair, true));
}
else if (pair.StartsWith("discard", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.Discard = true;
}
else if (pair.StartsWith("secure", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.Secure = true;
}
else if (pair.StartsWith("httponly", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.HttpOnly = true;
}
else
{
if (cookie != null)
cookies.Add(cookie);
cookie = ParseCookie(pair);
}
}
if (cookie != null)
cookies.Add(cookie);
return cookies;
}
/// <inheritdoc />
public new void Add(Cookie cookie)
{
if (cookie == null)
throw new ArgumentNullException(nameof(cookie));
var pos = SearchCookie(cookie);
if (pos == -1)
{
base.Add(cookie);
return;
}
this[pos] = cookie;
}
/// <inheritdoc />
public void CopyTo(Array array, int index)
{
if (array == null)
throw new ArgumentNullException(nameof(array));
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index), "Less than zero.");
if (array.Rank > 1)
throw new ArgumentException("Multidimensional.", nameof(array));
if (array.Length - index < Count)
{
throw new ArgumentException(
"The number of elements in this collection is greater than the available space of the destination array.");
}
if (array.GetType().GetElementType()?.IsAssignableFrom(typeof(Cookie)) != true)
{
throw new InvalidCastException(
"The elements in this collection cannot be cast automatically to the type of the destination array.");
}
((IList) this).CopyTo(array, index);
}
private static string? GetValue(string nameAndValue, bool unquote = false)
{
var idx = nameAndValue.IndexOf('=');
if (idx < 0 || idx == nameAndValue.Length - 1)
return null;
var val = nameAndValue.Substring(idx + 1).Trim();
return unquote ? val.Unquote() : val;
}
private static string[] SplitCookieHeaderValue(string value) => value.SplitHeaderValue(true).ToArray();
private static int CompareCookieWithinSorted(Cookie x, Cookie y)
{
var ret = x.Version - y.Version;
return ret != 0
? ret
: (ret = string.Compare(x.Name, y.Name, StringComparison.Ordinal)) != 0
? ret
: y.Path.Length - x.Path.Length;
}
private static Cookie ParseCookie(string pair)
{
string name;
var val = string.Empty;
var pos = pair.IndexOf('=');
if (pos == -1)
{
name = pair;
}
else if (pos == pair.Length - 1)
{
name = pair.Substring(0, pos).TrimEnd(' ');
}
else
{
name = pair.Substring(0, pos).TrimEnd(' ');
val = pair.Substring(pos + 1).TrimStart(' ');
}
return new Cookie(name, val);
}
private int SearchCookie(Cookie cookie)
{
var name = cookie.Name;
var path = cookie.Path;
var domain = cookie.Domain;
var ver = cookie.Version;
for (var i = Count - 1; i >= 0; i--)
{
var c = this[i];
if (c.Name.Equals(name, StringComparison.OrdinalIgnoreCase) &&
c.Path.Equals(path, StringComparison.OrdinalIgnoreCase) &&
c.Domain.Equals(domain, StringComparison.OrdinalIgnoreCase) &&
c.Version == ver)
return i;
}
return -1;
}
}
}

View File

@@ -0,0 +1,141 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using EmbedIO.Net.Internal;
using Swan.Logging;
namespace EmbedIO.Net
{
/// <summary>
/// Represents the EndPoint Manager.
/// </summary>
public static class EndPointManager
{
private static readonly ConcurrentDictionary<IPAddress, ConcurrentDictionary<int, EndPointListener>> IPToEndpoints = new ();
/// <summary>
/// Gets or sets a value indicating whether [use IPv6]. By default, this flag is set.
/// </summary>
/// <value>
/// <c>true</c> if [use IPv6]; otherwise, <c>false</c>.
/// </value>
public static bool UseIpv6 { get; set; } = true;
internal static void AddListener(HttpListener listener)
{
var added = new List<string>();
try
{
foreach (var prefix in listener.Prefixes)
{
AddPrefix(prefix, listener);
added.Add(prefix);
}
}
catch (Exception ex)
{
ex.Log(nameof(AddListener));
foreach (var prefix in added)
{
RemovePrefix(prefix, listener);
}
throw;
}
}
internal static void RemoveEndPoint(EndPointListener epl, IPEndPoint ep)
{
if (IPToEndpoints.TryGetValue(ep.Address, out var p))
{
if (p.TryRemove(ep.Port, out _) && p.Count == 0)
{
_ = IPToEndpoints.TryRemove(ep.Address, out _);
}
}
epl.Dispose();
}
internal static void RemoveListener(HttpListener listener)
{
foreach (var prefix in listener.Prefixes)
{
RemovePrefix(prefix, listener);
}
}
internal static void AddPrefix(string p, HttpListener listener)
{
var lp = new ListenerPrefix(p);
if (!lp.IsValid())
{
throw new HttpListenerException(400, "Invalid path.");
}
// listens on all the interfaces if host name cannot be parsed by IPAddress.
var epl = GetEpListener(lp.Host, lp.Port, listener, lp.Secure);
epl.AddPrefix(lp, listener);
}
private static EndPointListener GetEpListener(string host, int port, HttpListener listener, bool secure = false)
{
var address = ResolveAddress(host);
var p = IPToEndpoints.GetOrAdd(address, x => new ConcurrentDictionary<int, EndPointListener>());
return p.GetOrAdd(port, x => new EndPointListener(listener, address, x, secure));
}
private static IPAddress ResolveAddress(string host)
{
if (host == "*" || host == "+" || host == "0.0.0.0")
{
return UseIpv6 ? IPAddress.IPv6Any : IPAddress.Any;
}
if (IPAddress.TryParse(host, out var address))
{
return address;
}
try
{
var hostEntry = new IPHostEntry {
HostName = host,
AddressList = Dns.GetHostAddresses(host),
};
return hostEntry.AddressList[0];
}
catch
{
return UseIpv6 ? IPAddress.IPv6Any : IPAddress.Any;
}
}
private static void RemovePrefix(string prefix, HttpListener listener)
{
try
{
var lp = new ListenerPrefix(prefix);
if (!lp.IsValid())
{
return;
}
var epl = GetEpListener(lp.Host, lp.Port, listener, lp.Secure);
epl.RemovePrefix(lp);
}
catch (SocketException)
{
// ignored
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More