diff --git a/.idea/.idea.RemoteControl/.idea/.gitignore b/.idea/.idea.RemoteControl/.idea/.gitignore new file mode 100644 index 0000000..ba2b321 --- /dev/null +++ b/.idea/.idea.RemoteControl/.idea/.gitignore @@ -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/ diff --git a/.idea/.idea.RemoteControl/.idea/encodings.xml b/.idea/.idea.RemoteControl/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.RemoteControl/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.RemoteControl/.idea/indexLayout.xml b/.idea/.idea.RemoteControl/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.RemoteControl/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.RemoteControl/.idea/vcs.xml b/.idea/.idea.RemoteControl/.idea/vcs.xml new file mode 100644 index 0000000..8306744 --- /dev/null +++ b/.idea/.idea.RemoteControl/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/About/About.xml b/About/About.xml new file mode 100644 index 0000000..117fcc0 --- /dev/null +++ b/About/About.xml @@ -0,0 +1,11 @@ + + + RemoteControl + TheQuux + 1.0 + Provides an API for controlling + 0 + + LaunchPad + + diff --git a/About/Preview.png b/About/Preview.png new file mode 100644 index 0000000..a74ec24 Binary files /dev/null and b/About/Preview.png differ diff --git a/About/Thumb.png b/About/Thumb.png new file mode 100644 index 0000000..6d11a2b Binary files /dev/null and b/About/Thumb.png differ diff --git a/BepInEx.cs b/BepInEx.cs deleted file mode 100644 index 2aa98f9..0000000 --- a/BepInEx.cs +++ /dev/null @@ -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 -} diff --git a/ExamplePatchMod.csproj b/ExamplePatchMod.csproj deleted file mode 100644 index dc96939..0000000 --- a/ExamplePatchMod.csproj +++ /dev/null @@ -1,66 +0,0 @@ - - - - - Debug - AnyCPU - {81D8C460-4627-489B-8D5E-A0640866290F} - Library - Properties - ExamplePatchMod - ExamplePatchMod - 9.0 - v4.7.2 - 512 - true - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - D:\SteamLibrary\steamapps\common\Stationeers\BepInEx\core\0Harmony.dll - - - D:\SteamLibrary\steamapps\common\Stationeers\rocketstation_Data\Managed\Assembly-CSharp.dll - - - D:\SteamLibrary\steamapps\common\Stationeers\BepInEx\core\BepInEx.dll - - - - - - - - - - - D:\SteamLibrary\steamapps\common\Stationeers\rocketstation_Data\Managed\UnityEngine.dll - - - D:\SteamLibrary\steamapps\common\Stationeers\rocketstation_Data\Managed\UnityEngine.CoreModule.dll - - - - - - - - - - \ No newline at end of file diff --git a/Patches/ExamplePatchClass.cs b/Patches/ExamplePatchClass.cs deleted file mode 100644 index 9f96802..0000000 --- a/Patches/ExamplePatchClass.cs +++ /dev/null @@ -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 - { - } -} diff --git a/Patches/Tick.cs b/Patches/Tick.cs new file mode 100644 index 0000000..2cca631 --- /dev/null +++ b/Patches/Tick.cs @@ -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); + // } + } + + } + } + + +} \ No newline at end of file diff --git a/Patches/WorldManagerStop.cs b/Patches/WorldManagerStop.cs new file mode 100644 index 0000000..0578abc --- /dev/null +++ b/Patches/WorldManagerStop.cs @@ -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) + { + + } + } +} \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index e6c2917..c0b7746 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -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("")] diff --git a/README.md b/README.md index 06bf1cf..2c52f6e 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/RemoteControl.csproj b/RemoteControl.csproj new file mode 100644 index 0000000..3f58104 --- /dev/null +++ b/RemoteControl.csproj @@ -0,0 +1,410 @@ + + + + + Debug + AnyCPU + {81D8C460-4627-489B-8D5E-A0640866290F} + Library + Properties + RemoteControl + RemoteControl + 9.0 + v4.8.1 + 512 + true + enable + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + C:\Program Files (x86)\Steam\steamapps\common\Stationeers\BepInEx\core\0Harmony.dll + + + C:\Program Files (x86)\Steam\steamapps\common\Stationeers\rocketstation_Data\Managed\Assembly-CSharp.dll + + + C:\Program Files (x86)\Steam\steamapps\common\Stationeers\BepInEx\core\BepInEx.dll + + + + + + + + + + + + + + C:\Program Files (x86)\Steam\steamapps\common\Stationeers\rocketstation_Data\Managed\UniTask.dll + + + C:\Program Files (x86)\Steam\steamapps\common\Stationeers\rocketstation_Data\Managed\UnityEngine.dll + + + C:\Program Files (x86)\Steam\steamapps\common\Stationeers\rocketstation_Data\Managed\UnityEngine.CoreModule.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ExamplePatchMod.sln b/RemoteControl.sln similarity index 87% rename from ExamplePatchMod.sln rename to RemoteControl.sln index c263c3c..d602b0e 100644 --- a/ExamplePatchMod.sln +++ b/RemoteControl.sln @@ -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 diff --git a/Scripts/Remotecontrol.cs b/Scripts/Remotecontrol.cs new file mode 100644 index 0000000..3da3b87 --- /dev/null +++ b/Scripts/Remotecontrol.cs @@ -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 Port; + + // ReSharper disable once MemberCanBePrivate.Global + public static ConfigEntry 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()) + .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 prefabs) + { + // Start(); +#if DEVELOPMENT_BUILD + Debug.Log($"Loaded {prefabs.Count} prefabs"); +#endif + } + + public void OnUnloaded(List 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 LtByName = EnumCollections.LogicTypes.AsLookupDict(true); + private static readonly Dictionary 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(referenceId); + return dev == null ? null : GetDeviceJson(dev); + } + + [Route(HttpVerbs.Get, "/networks")] + public Task> ListNetworks() => Task.FromResult(SubscriptionManager.GetProbes()); + + [Route(HttpVerbs.Get, "/networks/{networkId}")] + public Task> ListDevices(string networkId) + { + // lock (SubscriptionManager.Lock) + using (new LogTimer("ListDevices")) + { + var networks = new HashSet(); + 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(); + 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>(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(); + + 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(); + 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>(); + 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 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; + } + } +} \ No newline at end of file diff --git a/Scripts/SubscriptionManager.cs b/Scripts/SubscriptionManager.cs new file mode 100644 index 0000000..47489c4 --- /dev/null +++ b/Scripts/SubscriptionManager.cs @@ -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 Probes = new (); + internal CableNetwork Network { get; private set; } + + private readonly HashSet _knownIds = new(); + + + internal readonly Dictionary 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(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(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()); + } + + 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 + { + 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 + { + 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 DataNetworks = new(); + // private Dictionary> _probesByName = new(); + // private Dictionary _probesById = new(); + private static readonly Dictionary PendingUpdates = new(); + private static readonly Dictionary 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 GetDataNetwork(string probeName) + { + // lock (Lock) + { + List 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(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(referenceID); + if (GetDataNetwork(probeName).Any(network => network.DeviceCache.ContainsKey(referenceID))) + { + return device; + } + return null; + } + + /// + /// Called from Unity thread pool before the logic tick + /// + /// + 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(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 scannedAnalyzers = new(); + HashSet scannedNetworks = new(); + + scannedNetworks.Clear(); + GameDevice.AllDevices.ForEach(dev => + { + if (dev is CableAnalyser analyser) + { + scannedAnalyzers.Add(analyser); + scannedNetworks.Add(analyser.CableNetwork); + } + }); + + var removed = + new List(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 GetProbes() + { + List 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); + } + } +} \ No newline at end of file diff --git a/Scripts/SubscriptionModule.cs b/Scripts/SubscriptionModule.cs new file mode 100644 index 0000000..351693d --- /dev/null +++ b/Scripts/SubscriptionModule.cs @@ -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 _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 Networks { get; set; } = new(); + } + + public class Network + { + public long ReferenceId { get; set; } + public List Probes { get; set; } = new(); + + public List 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 LogicValues { get; set; } = new(); + public List> Slots { get; set; }= new(); + } + } +} \ No newline at end of file diff --git a/Utils/Extensions.cs b/Utils/Extensions.cs new file mode 100644 index 0000000..95f0305 --- /dev/null +++ b/Utils/Extensions.cs @@ -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 AsLookupDict(this EnumCollection collection, bool lowercase) + where TEnum : Enum, IConvertible, new() + where TValue : IConvertible, IEquatable + { + var result = new Dictionary(); + 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; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Actions/ActionModule.cs b/Vendor/EmbedIO-3.5.2/Actions/ActionModule.cs new file mode 100644 index 0000000..8089d51 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Actions/ActionModule.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using EmbedIO.Utilities; + +namespace EmbedIO.Actions +{ + /// + /// A module that passes requests to a callback. + /// + /// + public class ActionModule : WebModuleBase + { + private readonly HttpVerbs _verb; + + private readonly RequestHandlerCallback _handler; + + /// + /// Initializes a new instance of the class. + /// + /// The base route. + /// The HTTP verb that will be served by this module. + /// The callback used to handle requests. + /// is . + /// + public ActionModule(string baseRoute, HttpVerbs verb, RequestHandlerCallback handler) + : base(baseRoute) + { + _verb = verb; + _handler = Validate.NotNull(nameof(handler), handler); + } + + /// + /// Initializes a new instance of the class. + /// + /// The handler. + public ActionModule(RequestHandlerCallback handler) + : this("/", HttpVerbs.Any, handler) + { + } + + /// + public override bool IsFinalHandler => false; + + /// + protected override async Task OnRequestAsync(IHttpContext context) + { + if (_verb != HttpVerbs.Any && context.Request.HttpVerb != _verb) + return; + + await _handler(context).ConfigureAwait(false); + context.SetHandled(); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Actions/RedirectModule.cs b/Vendor/EmbedIO-3.5.2/Actions/RedirectModule.cs new file mode 100644 index 0000000..5b2913f --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Actions/RedirectModule.cs @@ -0,0 +1,99 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using EmbedIO.Utilities; + +namespace EmbedIO.Actions +{ + /// + /// A module that redirects requests. + /// + /// + public class RedirectModule : WebModuleBase + { + private readonly Func? _shouldRedirect; + + /// + /// Initializes a new instance of the class + /// that will redirect all served requests. + /// + /// The base route. + /// The redirect URL. + /// The response status code; default is 302 - Found. + /// is . + /// + /// is not a valid URL. + /// - or - + /// is not a redirection (3xx) status code. + /// + /// + public RedirectModule(string baseRoute, string redirectUrl, HttpStatusCode statusCode = HttpStatusCode.Found) + : this(baseRoute, redirectUrl, null, statusCode, false) + { + } + + /// + /// Initializes a new instance of the class + /// that will redirect all requests for which the callback + /// returns . + /// + /// The base route. + /// The redirect URL. + /// A callback function that returns + /// if a request must be redirected. + /// The response status code; default is 302 - Found. + /// + /// is . + /// - or - + /// is . + /// + /// + /// is not a valid URL. + /// - or - + /// is not a redirection (3xx) status code. + /// + /// + public RedirectModule(string baseRoute, string redirectUrl, Func? shouldRedirect, HttpStatusCode statusCode = HttpStatusCode.Found) + : this(baseRoute, redirectUrl, shouldRedirect, statusCode, true) + { + } + + private RedirectModule(string baseRoute, string redirectUrl, Func? 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; + } + + /// + public override bool IsFinalHandler => false; + + /// + /// Gets the redirect URL. + /// + public string RedirectUrl { get; } + + /// + /// Gets the response status code. + /// + public HttpStatusCode StatusCode { get; } + + /// + protected override Task OnRequestAsync(IHttpContext context) + { + if (_shouldRedirect?.Invoke(context) ?? true) + { + context.Redirect(RedirectUrl, (int)StatusCode); + context.SetHandled(); + } + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Authentication/Auth.cs b/Vendor/EmbedIO-3.5.2/Authentication/Auth.cs new file mode 100644 index 0000000..25b3a08 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Authentication/Auth.cs @@ -0,0 +1,31 @@ +using System.Security.Principal; + +namespace EmbedIO.Authentication +{ + /// + /// Provides useful authentication-related constants. + /// + public static class Auth + { + /// + /// Gets an interface representing + /// no user. To be used instead of + /// to initialize or set properties of type . + /// + public static IPrincipal NoUser { get; } = new GenericPrincipal( + new GenericIdentity(string.Empty, string.Empty), + null); + + /// + /// Creates and returns an interface + /// representing an unauthenticated user, with the given + /// authentication type. + /// + /// The type of authentication used to identify the user. + /// An interface. + public static IPrincipal CreateUnauthenticatedPrincipal(string authenticationType) + => new GenericPrincipal( + new GenericIdentity(string.Empty, authenticationType), + null); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Authentication/BasicAuthenticationModule.cs b/Vendor/EmbedIO-3.5.2/Authentication/BasicAuthenticationModule.cs new file mode 100644 index 0000000..e76e34f --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Authentication/BasicAuthenticationModule.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace EmbedIO.Authentication +{ + /// + /// Simple HTTP basic authentication module that stores credentials + /// in a . + /// + public class BasicAuthenticationModule : BasicAuthenticationModuleBase + { + /// + /// Initializes a new instance of the class. + /// + /// The base route. + /// The authentication realm. + /// + /// If is or the empty string, + /// the Realm property will be set equal to + /// BaseRoute. + /// + public BasicAuthenticationModule(string baseRoute, string? realm = null) + : base(baseRoute, realm) + { + } + + /// + /// Gets a dictionary of valid user names and passwords. + /// + /// + /// The accounts. + /// + public ConcurrentDictionary Accounts { get; } = new ConcurrentDictionary(StringComparer.InvariantCulture); + + /// + protected override Task 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); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Authentication/BasicAuthenticationModuleBase.cs b/Vendor/EmbedIO-3.5.2/Authentication/BasicAuthenticationModuleBase.cs new file mode 100644 index 0000000..e618c9a --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Authentication/BasicAuthenticationModuleBase.cs @@ -0,0 +1,103 @@ +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EmbedIO.Authentication +{ + /// + /// Implements HTTP basic authentication. + /// + public abstract class BasicAuthenticationModuleBase : WebModuleBase + { + private readonly string _wwwAuthenticateHeaderValue; + + /// + /// Initializes a new instance of the class. + /// + /// The base URL path. + /// The authentication realm. + /// + /// If is or the empty string, + /// the property will be set equal to + /// BaseRoute. + /// + protected BasicAuthenticationModuleBase(string baseRoute, string? realm) + : base(baseRoute) + { + Realm = string.IsNullOrEmpty(realm) ? BaseRoute : realm; + + _wwwAuthenticateHeaderValue = $"Basic realm=\"{Realm}\" charset=UTF-8"; + } + + /// + public sealed override bool IsFinalHandler => false; + + /// + /// Gets the authentication realm. + /// + public string Realm { get; } + + /// + protected sealed override async Task OnRequestAsync(IHttpContext context) + { + async Task 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(); + } + + /// + /// Verifies the credentials given in the Authentication request header. + /// + /// The URL path requested by the client. Note that this is relative + /// to the module's BaseRoute. + /// The user name, or if none has been given. + /// The password, or if none has been given. + /// A use to cancel the operation. + /// A whose result will be if the given credentials + /// are valid, if they are not. + protected abstract Task 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)); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Authentication/BasicAuthenticationModuleExtensions.cs b/Vendor/EmbedIO-3.5.2/Authentication/BasicAuthenticationModuleExtensions.cs new file mode 100644 index 0000000..448ecec --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Authentication/BasicAuthenticationModuleExtensions.cs @@ -0,0 +1,34 @@ +using System; + +namespace EmbedIO.Authentication +{ + /// + /// Provides extension methods for . + /// + public static class BasicAuthenticationModuleExtensions + { + /// + /// Adds a username and password to the Accounts dictionary. + /// + /// The on which this method is called. + /// The user name. + /// The password. + /// , with the user name and password added. + /// is . + /// is . + /// + /// The Accounts dictionary already contains + /// the maximum number of elements (MaxValue). + /// + /// + /// If a account already exists, + /// its password is replaced with . + /// + public static BasicAuthenticationModule WithAccount(this BasicAuthenticationModule @this, string userName, string password) + { + @this.Accounts.AddOrUpdate(userName, password, (_, __) => password); + + return @this; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/CompressionMethod.cs b/Vendor/EmbedIO-3.5.2/CompressionMethod.cs new file mode 100644 index 0000000..82f21cf --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/CompressionMethod.cs @@ -0,0 +1,29 @@ +namespace EmbedIO +{ + /// + /// Specifies the compression method used to compress a message on + /// the WebSocket connection. + /// + /// + /// The compression methods that can be used are defined in + /// + /// Compression Extensions for WebSocket. + /// + public enum CompressionMethod : byte + { + /// + /// Specifies no compression. + /// + None, + + /// + /// Specifies "Deflate" compression. + /// + Deflate, + + /// + /// Specifies GZip compression. + /// + Gzip, + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/CompressionMethodNames.cs b/Vendor/EmbedIO-3.5.2/CompressionMethodNames.cs new file mode 100644 index 0000000..3511b96 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/CompressionMethodNames.cs @@ -0,0 +1,27 @@ +namespace EmbedIO +{ + /// + /// Exposes constants for possible values of the Content-Encoding HTTP header. + /// + /// + public static class CompressionMethodNames + { + /// + /// Specifies no compression. + /// + /// + public const string None = "identity"; + + /// + /// Specifies the "Deflate" compression method. + /// + /// + public const string Deflate = "deflate"; + + /// + /// Specifies the GZip compression method. + /// + /// + public const string Gzip = "gzip"; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Cors/CorsModule.cs b/Vendor/EmbedIO-3.5.2/Cors/CorsModule.cs new file mode 100644 index 0000000..ee9bafd --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Cors/CorsModule.cs @@ -0,0 +1,130 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using EmbedIO.Utilities; + +namespace EmbedIO.Cors +{ + /// + /// 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. + /// + public class CorsModule : WebModuleBase + { + /// + /// A string meaning "All" in CORS headers. + /// + public const string All = "*"; + + private readonly string _origins; + private readonly string _headers; + private readonly string _methods; + private readonly string[] _validOrigins; + private readonly string[] _validMethods; + + /// + /// Initializes a new instance of the class. + /// + /// The base route. + /// The valid origins. The default is (*). + /// The valid headers. The default is (*). + /// The valid methods. The default is (*). + /// + /// origins + /// or + /// headers + /// or + /// methods + /// + 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(); + } + + /// + public override bool IsFinalHandler => false; + + /// + 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); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/EmbedIOInternalErrorException.cs b/Vendor/EmbedIO-3.5.2/EmbedIOInternalErrorException.cs new file mode 100644 index 0000000..4d800b8 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/EmbedIOInternalErrorException.cs @@ -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 + /// + /// The exception that is thrown by EmbedIO's internal diagnostic checks to signal a condition + /// most probably caused by an error in EmbedIO. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + [Serializable] + public class EmbedIOInternalErrorException : Exception + { + /// + /// Initializes a new instance of the class. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + public EmbedIOInternalErrorException() + { + } + + /// + /// Initializes a new instance of the class. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + /// The message that describes the error. + public EmbedIOInternalErrorException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, + /// or if no inner exception is specified. + public EmbedIOInternalErrorException(string message, Exception? innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + protected EmbedIOInternalErrorException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +#pragma warning restore SA1642 +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/ExceptionHandler.cs b/Vendor/EmbedIO-3.5.2/ExceptionHandler.cs new file mode 100644 index 0000000..6355889 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/ExceptionHandler.cs @@ -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 +{ + /// + /// Provides standard handlers for unhandled exceptions at both module and server level. + /// + /// + /// + public static class ExceptionHandler + { + /// + /// The name of the response header used by the + /// handler to transmit the type of the exception to the client. + /// + public const string ExceptionTypeHeaderName = "X-Exception-Type"; + + /// + /// The name of the response header used by the + /// handler to transmit the message of the exception to the client. + /// + public const string ExceptionMessageHeaderName = "X-Exception-Message"; + + /// + /// Gets or sets the contact information to include in exception responses. + /// + public static string? ContactInformation { get; set; } + + /// + /// Gets or sets a value indicating whether to include stack traces + /// in exception responses. + /// + public static bool IncludeStackTraces { get; set; } + + /// + /// Gets the default handler used by . + /// This is the same as . + /// + public static ExceptionHandlerCallback Default { get; } = HtmlResponse; + + /// + /// Sends an empty 500 Internal Server Error response. + /// + /// An interface representing the context of the request. + /// The unhandled exception. + /// A representing the ongoing operation. +#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; + } + + /// + /// Sends an empty 500 Internal Server Error response, + /// with the following additional headers: + /// + /// + /// Header + /// Value + /// + /// + /// X-Exception-Type + /// The name (without namespace) of the type of exception that was thrown. + /// + /// + /// X-Exception-Message + /// The Message property of the exception. + /// + /// + /// The aforementioned header names are available as the and + /// properties, respectively. + /// + /// An interface representing the context of the request. + /// The unhandled exception. + /// A representing the ongoing operation. + 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; + } + + /// + /// Sends a 500 Internal Server Error response with a HTML payload + /// briefly describing the error, including contact information and/or a stack trace + /// if specified via the and + /// properties, respectively. + /// + /// An interface representing the context of the request. + /// The unhandled exception. + /// A representing the ongoing operation. + public static Task HtmlResponse(IHttpContext context, Exception exception) + => context.SendStandardHtmlAsync( + (int)HttpStatusCode.InternalServerError, + text => + { + text.Write("

The server has encountered an error and was not able to process your request.

"); + text.Write("

Please contact the server administrator"); + + if (!string.IsNullOrEmpty(ContactInformation)) + text.Write(" ({0})", WebUtility.HtmlEncode(ContactInformation)); + + text.Write(", informing them of the time this error occurred and the action(s) you performed that resulted in this error.

"); + text.Write("

The following information may help them in finding out what happened and restoring full functionality.

"); + text.Write( + "

Exception type: {0}

Message: {1}", + WebUtility.HtmlEncode(exception.GetType().FullName ?? ""), + WebUtility.HtmlEncode(exception.Message)); + + if (IncludeStackTraces) + { + text.Write( + "

Stack trace:


{0}
", + WebUtility.HtmlEncode(exception.StackTrace)); + } + }); + + internal static async Task Handle(string logSource, IHttpContext context, Exception exception, ExceptionHandlerCallback? handler, HttpExceptionHandlerCallback? httpHandler) + { + if (handler == null) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + return; + } + + exception.Log(logSource, $"[{context.Id}] Unhandled exception."); + + try + { + context.Response.SetEmptyResponse((int)HttpStatusCode.InternalServerError); + context.Response.DisableCaching(); + await handler(context, exception) + .ConfigureAwait(false); + } + catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) + { + throw; + } + catch (HttpListenerException) + { + throw; + } + catch (Exception httpException) when (httpException is IHttpException httpException1) + { + if (httpHandler == null) + throw; + + await httpHandler(context, httpException1).ConfigureAwait(false); + } + catch (Exception exception2) + { + exception2.Log(logSource, $"[{context.Id}] Unhandled exception while handling exception."); + } + } + } +} diff --git a/Vendor/EmbedIO-3.5.2/ExceptionHandlerCallback.cs b/Vendor/EmbedIO-3.5.2/ExceptionHandlerCallback.cs new file mode 100644 index 0000000..406dda2 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/ExceptionHandlerCallback.cs @@ -0,0 +1,22 @@ +using System; +using System.Net; +using System.Threading.Tasks; + +namespace EmbedIO +{ + /// + /// A callback used to provide information about an unhandled exception occurred while processing a request. + /// + /// An interface representing the context of the request. + /// The unhandled exception. + /// A representing the ongoing operation. + /// + /// When this delegate is called, the response's status code has already been set to + /// . + /// Any exception thrown by a handler (even a HTTP exception) will go unhandled: the web server + /// will not crash, but processing of the request will be aborted, and the response will be flushed as-is. + /// In other words, it is not a good ides to throw HttpException.NotFound() (or similar) + /// from a handler. + /// + public delegate Task ExceptionHandlerCallback(IHttpContext context, Exception exception); +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/DirectoryLister.cs b/Vendor/EmbedIO-3.5.2/Files/DirectoryLister.cs new file mode 100644 index 0000000..d99ee89 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/DirectoryLister.cs @@ -0,0 +1,20 @@ +using EmbedIO.Files.Internal; + +namespace EmbedIO.Files +{ + /// + /// Provides standard directory listers for . + /// + /// + public static class DirectoryLister + { + /// + /// Gets an interface + /// that produces a HTML listing of a directory. + /// The output of the returned directory lister + /// is the same as a directory listing obtained + /// by EmbedIO version 2. + /// + public static IDirectoryLister Html => HtmlDirectoryLister.Instance; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/FileCache.Section.cs b/Vendor/EmbedIO-3.5.2/Files/FileCache.Section.cs new file mode 100644 index 0000000..e133f14 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/FileCache.Section.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using EmbedIO.Files.Internal; + +namespace EmbedIO.Files +{ + public sealed partial class FileCache + { + internal class Section + { + private readonly object _syncRoot = new object(); + private readonly Dictionary _items = new Dictionary(StringComparer.Ordinal); + private long _totalSize; + private string? _oldestKey; + private string? _newestKey; + + public void Clear() + { + lock (_syncRoot) + { + ClearCore(); + } + } + + public void Add(string path, FileCacheItem item) + { + lock (_syncRoot) + { + AddItemCore(path, item); + } + } + + public void Remove(string path) + { + lock (_syncRoot) + { + RemoveItemCore(path); + } + } + + public bool TryGet(string path, out FileCacheItem item) + { + lock (_syncRoot) + { + if (!_items.TryGetValue(path, out item)) + return false; + + RefreshItemCore(path, item); + return true; + } + } + + internal long GetLeastRecentUseTime() + { + lock (_syncRoot) + { + return _oldestKey == null ? long.MaxValue : _items[_oldestKey].LastUsedAt; + } + } + + // Removes least recently used item. + // Returns size of removed item. + internal long RemoveLeastRecentItem() + { + lock (_syncRoot) + { + return RemoveLeastRecentItemCore(); + } + } + + internal long GetTotalSize() + { + lock (_syncRoot) + { + return _totalSize; + } + } + + internal void UpdateTotalSize(long delta) + { + lock (_syncRoot) + { + _totalSize += delta; + } + } + + private void ClearCore() + { + _items.Clear(); + _totalSize = 0; + _oldestKey = null; + _newestKey = null; + } + + // Adds an item as most recently used. + private void AddItemCore(string path, FileCacheItem item) + { + item.PreviousKey = _newestKey; + item.NextKey = null; + item.LastUsedAt = TimeBase.ElapsedTicks; + + if (_newestKey != null) + _items[_newestKey].NextKey = path; + + _newestKey = path; + + _items[path] = item; + _totalSize += item.SizeInCache; + } + + // Removes an item. + private void RemoveItemCore(string path) + { + if (!_items.TryGetValue(path, out var item)) + return; + + if (_oldestKey == path) + _oldestKey = item.NextKey; + + if (_newestKey == path) + _newestKey = item.PreviousKey; + + if (item.PreviousKey != null) + _items[item.PreviousKey].NextKey = item.NextKey; + + if (item.NextKey != null) + _items[item.NextKey].PreviousKey = item.PreviousKey; + + item.PreviousKey = null; + item.NextKey = null; + + _items.Remove(path); + _totalSize -= item.SizeInCache; + } + + // Removes the least recently used item. + // returns size of removed item. + private long RemoveLeastRecentItemCore() + { + var path = _oldestKey; + if (path == null) + return 0; + + var item = _items[path]; + + if ((_oldestKey = item.NextKey) != null) + _items[_oldestKey].PreviousKey = null; + + if (_newestKey == path) + _newestKey = null; + + item.PreviousKey = null; + item.NextKey = null; + + _items.Remove(path); + _totalSize -= item.SizeInCache; + return item.SizeInCache; + } + + // Moves an item to most recently used. + private void RefreshItemCore(string path, FileCacheItem item) + { + item.LastUsedAt = TimeBase.ElapsedTicks; + + if (_newestKey == path) + return; + + if (_oldestKey == path) + _oldestKey = item.NextKey; + + if (item.PreviousKey != null) + _items[item.PreviousKey].NextKey = item.NextKey; + + if (item.NextKey != null) + _items[item.NextKey].PreviousKey = item.PreviousKey; + + item.PreviousKey = _newestKey; + item.NextKey = null; + + _items[_newestKey!].NextKey = path; + _newestKey = path; + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/FileCache.cs b/Vendor/EmbedIO-3.5.2/Files/FileCache.cs new file mode 100644 index 0000000..accf545 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/FileCache.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Internal; +using Swan.Threading; +using Swan.Logging; + +namespace EmbedIO.Files +{ +#pragma warning disable CA1001 // Type owns disposable field '_cleaner' but is not disposable - _cleaner has its own dispose semantics. + /// + /// A cache where one or more instances of can store hashes and file contents. + /// + public sealed partial class FileCache +#pragma warning restore CA1001 + { + /// + /// The default value for the property. + /// + public const int DefaultMaxSizeKb = 10240; + + /// + /// The default value for the property. + /// + public const int DefaultMaxFileSizeKb = 200; + + private static readonly Stopwatch TimeBase = Stopwatch.StartNew(); + + private static readonly object DefaultSyncRoot = new object(); + private static FileCache? _defaultInstance; + + private readonly ConcurrentDictionary _sections = new ConcurrentDictionary(StringComparer.Ordinal); + private int _sectionCount; // Because ConcurrentDictionary<,>.Count is locking. + private int _maxSizeKb = DefaultMaxSizeKb; + private int _maxFileSizeKb = DefaultMaxFileSizeKb; + private PeriodicTask? _cleaner; + + /// + /// Gets the default instance used by . + /// + public static FileCache Default + { + get + { + if (_defaultInstance != null) + return _defaultInstance; + + lock (DefaultSyncRoot) + { + if (_defaultInstance == null) + _defaultInstance = new FileCache(); + } + + return _defaultInstance; + } + } + + /// + /// Gets or sets the maximum total size of cached data in kilobytes (1 kilobyte = 1024 bytes). + /// The default value for this property is stored in the constant field. + /// Setting this property to a value less lower han 1 has the same effect as setting it to 1. + /// + public int MaxSizeKb + { + get => _maxSizeKb; + set => _maxSizeKb = Math.Max(value, 1); + } + + /// + /// Gets or sets the maximum size of a single cached file in kilobytes (1 kilobyte = 1024 bytes). + /// A single file's contents may be present in a cache more than once, if the file + /// is requested with different Accept-Encoding request headers. This property acts as a threshold + /// for the uncompressed size of a file. + /// The default value for this property is stored in the constant field. + /// Setting this property to a value lower than 0 has the same effect as setting it to 0, in fact + /// completely disabling the caching of file contents for this cache. + /// This property cannot be set to a value higher than 2097151; in other words, it is not possible + /// to cache files bigger than two Gigabytes (1 Gigabyte = 1048576 kilobytes) minus 1 kilobyte. + /// + public int MaxFileSizeKb + { + get => _maxFileSizeKb; + set => _maxFileSizeKb = Math.Min(Math.Max(value, 0), 2097151); + } + + // Cast as IDictionary because we WANT an exception to be thrown if the name exists. + // It would mean that something is very, very wrong. + internal Section AddSection(string name) + { + var section = new Section(); + (_sections as IDictionary).Add(name, section); + + if (Interlocked.Increment(ref _sectionCount) == 1) + _cleaner = new PeriodicTask(TimeSpan.FromMinutes(1), CheckMaxSize); + + return section; + } + + internal void RemoveSection(string name) + { + _sections.TryRemove(name, out _); + + if (Interlocked.Decrement(ref _sectionCount) == 0) + { + _cleaner?.Dispose(); + _cleaner = null; + } + } + + private async Task CheckMaxSize(CancellationToken cancellationToken) + { + var timeKeeper = new TimeKeeper(); + var maxSizeKb = _maxSizeKb; + var initialSizeKb = ComputeTotalSize() / 1024L; + + if (initialSizeKb <= maxSizeKb) + { + $"Total size = {initialSizeKb}/{_maxSizeKb}kb, not purging.".Debug(nameof(FileCache)); + return; + } + + $"Total size = {initialSizeKb}/{_maxSizeKb}kb, purging...".Debug(nameof(FileCache)); + + var removedCount = 0; + var removedSize = 0L; + var totalSizeKb = initialSizeKb; + var threshold = 973L * maxSizeKb / 1024L; // About 95% of maximum allowed size + while (totalSizeKb > threshold) + { + if (cancellationToken.IsCancellationRequested) + return; + + var section = GetSectionWithLeastRecentItem(); + if (section == null) + return; + + removedSize += section.RemoveLeastRecentItem(); + removedCount++; + + await Task.Yield(); + + totalSizeKb = ComputeTotalSize() / 1024L; + } + + $"Purge completed in {timeKeeper.ElapsedTime}ms: removed {removedCount} items ({removedSize / 1024L}kb). Total size is now {totalSizeKb}kb." + .Debug(nameof(FileCache)); + } + + // Enumerate key / value pairs because the Keys and Values property + // of ConcurrentDictionary<,> have snapshot semantics, + // while GetEnumerator enumerates without locking. + private long ComputeTotalSize() + => _sections.Sum(pair => pair.Value.GetTotalSize()); + + private Section? GetSectionWithLeastRecentItem() + { + Section? result = null; + var earliestTime = long.MaxValue; + foreach (var pair in _sections) + { + var section = pair.Value; + var time = section.GetLeastRecentUseTime(); + + if (time < earliestTime) + { + result = section; + earliestTime = time; + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/FileModule.cs b/Vendor/EmbedIO-3.5.2/Files/FileModule.cs new file mode 100644 index 0000000..5dccd56 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/FileModule.cs @@ -0,0 +1,635 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Files.Internal; +using EmbedIO.Internal; +using EmbedIO.Utilities; + +namespace EmbedIO.Files +{ + /// + /// A module serving files and directory listings from an . + /// + /// + public class FileModule : WebModuleBase, IDisposable, IMimeTypeCustomizer + { + /// + /// Default value for . + /// + public const string DefaultDocumentName = "index.html"; + + private readonly string _cacheSectionName = UniqueIdGenerator.GetNext(); + private readonly MimeTypeCustomizer _mimeTypeCustomizer = new MimeTypeCustomizer(); + private readonly ConcurrentDictionary? _mappingCache; + + private FileCache _cache = FileCache.Default; + private bool _contentCaching = true; + private string? _defaultDocument = DefaultDocumentName; + private string? _defaultExtension; + private IDirectoryLister? _directoryLister; + private FileRequestHandlerCallback _onMappingFailed = FileRequestHandler.ThrowNotFound; + private FileRequestHandlerCallback _onDirectoryNotListable = FileRequestHandler.ThrowUnauthorized; + private FileRequestHandlerCallback _onMethodNotAllowed = FileRequestHandler.ThrowMethodNotAllowed; + + private FileCache.Section? _cacheSection; + + /// + /// Initializes a new instance of the class, + /// using the specified cache. + /// + /// The base route. + /// An interface that provides access + /// to actual files and directories. + /// is . + public FileModule(string baseRoute, IFileProvider provider) + : base(baseRoute) + { + Provider = Validate.NotNull(nameof(provider), provider); + _mappingCache = Provider.IsImmutable + ? new ConcurrentDictionary() + : null; + } + + /// + /// Finalizes an instance of the class. + /// + ~FileModule() + { + Dispose(false); + } + + /// + public override bool IsFinalHandler => true; + + /// + /// Gets the interface that provides access + /// to actual files and directories served by this module. + /// + public IFileProvider Provider { get; } + + /// + /// Gets or sets the used by this module to store hashes and, + /// optionally, file contents and rendered directory listings. + /// + /// The module's configuration is locked. + /// This property is being set to . + public FileCache Cache + { + get => _cache; + set + { + EnsureConfigurationNotLocked(); + _cache = Validate.NotNull(nameof(value), value); + } + } + + /// + /// Gets or sets a value indicating whether this module caches the contents of files + /// and directory listings. + /// Note that the actual representations of files are stored in ; + /// thus, for example, if a file is always requested with an Accept-Encoding of gzip, + /// only the gzipped contents of the file will be cached. + /// + /// The module's configuration is locked. + public bool ContentCaching + { + get => _contentCaching; + set + { + EnsureConfigurationNotLocked(); + _contentCaching = value; + } + } + + /// + /// Gets or sets the name of the default document served, if it exists, instead of a directory listing + /// when the path of a requested URL maps to a directory. + /// The default value for this property is the constant. + /// + /// The module's configuration is locked. + public string? DefaultDocument + { + get => _defaultDocument; + set + { + EnsureConfigurationNotLocked(); + _defaultDocument = string.IsNullOrEmpty(value) ? null : value; + } + } + + /// + /// Gets or sets the default extension appended to requested URL paths that do not map + /// to any file or directory. Defaults to . + /// + /// The module's configuration is locked. + /// This property is being set to a non-, + /// non-empty string that does not start with a period (.). + public string? DefaultExtension + { + get => _defaultExtension; + set + { + EnsureConfigurationNotLocked(); + + if (string.IsNullOrEmpty(value)) + { + _defaultExtension = null; + } + else if (value![0] != '.') + { + throw new ArgumentException("Default extension does not start with a period.", nameof(value)); + } + else + { + _defaultExtension = value; + } + } + } + + /// + /// Gets or sets the interface used to generate + /// directory listing in this module. + /// A value of (the default) disables the generation + /// of directory listings. + /// + /// The module's configuration is locked. + public IDirectoryLister? DirectoryLister + { + get => _directoryLister; + set + { + EnsureConfigurationNotLocked(); + _directoryLister = value; + } + } + + /// + /// Gets or sets a that is called whenever + /// the requested URL path could not be mapped to any file or directory. + /// The default is . + /// + /// The module's configuration is locked. + /// This property is being set to . + /// + public FileRequestHandlerCallback OnMappingFailed + { + get => _onMappingFailed; + set + { + EnsureConfigurationNotLocked(); + _onMappingFailed = Validate.NotNull(nameof(value), value); + } + } + + /// + /// Gets or sets a that is called whenever + /// the requested URL path has been mapped to a directory, but directory listing has been + /// disabled by setting to . + /// The default is . + /// + /// The module's configuration is locked. + /// This property is being set to . + /// + public FileRequestHandlerCallback OnDirectoryNotListable + { + get => _onDirectoryNotListable; + set + { + EnsureConfigurationNotLocked(); + _onDirectoryNotListable = Validate.NotNull(nameof(value), value); + } + } + + /// + /// Gets or sets a that is called whenever + /// the requested URL path has been mapped to a file or directory, but the request's + /// HTTP method is neither GET nor HEAD. + /// The default is . + /// + /// The module's configuration is locked. + /// This property is being set to . + /// + public FileRequestHandlerCallback OnMethodNotAllowed + { + get => _onMethodNotAllowed; + set + { + EnsureConfigurationNotLocked(); + _onMethodNotAllowed = Validate.NotNull(nameof(value), value); + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + string IMimeTypeProvider.GetMimeType(string extension) + => _mimeTypeCustomizer.GetMimeType(extension); + + bool IMimeTypeProvider.TryDetermineCompression(string mimeType, out bool preferCompression) + => _mimeTypeCustomizer.TryDetermineCompression(mimeType, out preferCompression); + + /// + public void AddCustomMimeType(string extension, string mimeType) + => _mimeTypeCustomizer.AddCustomMimeType(extension, mimeType); + + /// + public void PreferCompression(string mimeType, bool preferCompression) + => _mimeTypeCustomizer.PreferCompression(mimeType, preferCompression); + + /// + /// Clears the part of used by this module. + /// + public void ClearCache() + { + _mappingCache?.Clear(); + _cacheSection?.Clear(); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!disposing) + return; + + if (_cacheSection != null) + Provider.ResourceChanged -= _cacheSection.Remove; + + if (Provider is IDisposable disposableProvider) + disposableProvider.Dispose(); + + if (_cacheSection != null) + Cache.RemoveSection(_cacheSectionName); + } + + /// + protected override void OnBeforeLockConfiguration() + { + base.OnBeforeLockConfiguration(); + + _mimeTypeCustomizer.Lock(); + } + + /// + protected override void OnStart(CancellationToken cancellationToken) + { + base.OnStart(cancellationToken); + + _cacheSection = Cache.AddSection(_cacheSectionName); + Provider.ResourceChanged += _cacheSection.Remove; + Provider.Start(cancellationToken); + } + + /// + protected override async Task OnRequestAsync(IHttpContext context) + { + MappedResourceInfo? info; + + var path = context.RequestedPath; + + // Map the URL path to a mapped resource. + // DefaultDocument and DefaultExtension are handled here. + // Use the mapping cache if it exists. + if (_mappingCache == null) + { + info = MapUrlPath(path, context); + } + else if (!_mappingCache.TryGetValue(path, out info)) + { + info = MapUrlPath(path, context); + if (info != null) + _ = _mappingCache.AddOrUpdate(path, info, (_, __) => info); + } + + if (info == null) + { + // If mapping failed, send a "404 Not Found" response, or whatever OnMappingFailed chooses to do. + // For example, it may return a default resource (think a folder of images and an imageNotFound.jpg), + // or redirect the request. + await OnMappingFailed(context, null).ConfigureAwait(false); + } + else if (!IsHttpMethodAllowed(context.Request, out var sendResponseBody)) + { + // If there is a mapped resource, check that the HTTP method is either GET or HEAD. + // Otherwise, send a "405 Method Not Allowed" response, or whatever OnMethodNotAllowed chooses to do. + await OnMethodNotAllowed(context, info).ConfigureAwait(false); + } + else if (info.IsDirectory && DirectoryLister == null) + { + // If a directory listing was requested, but there is no DirectoryLister, + // send a "403 Unauthorized" response, or whatever OnDirectoryNotListable chooses to do. + // For example, one could prefer to send "404 Not Found" instead. + await OnDirectoryNotListable(context, info).ConfigureAwait(false); + } + else + { + await HandleResource(context, info, sendResponseBody).ConfigureAwait(false); + } + } + + // Tells whether a request's HTTP method is suitable for processing by FileModule + // and, if so, whether a response body must be sent. + private static bool IsHttpMethodAllowed(IHttpRequest request, out bool sendResponseBody) + { + switch (request.HttpVerb) + { + case HttpVerbs.Head: + sendResponseBody = false; + return true; + case HttpVerbs.Get: + sendResponseBody = true; + return true; + default: + sendResponseBody = default; + return false; + } + } + + // Prepares response headers for a "200 OK" or "304 Not Modified" response. + // RFC7232, Section 4.1 + private static void PreparePositiveResponse(IHttpResponse response, MappedResourceInfo info, string contentType, string entityTag, Action setCompression) + { + setCompression(response); + response.ContentType = contentType; + response.Headers.Set(HttpHeaderNames.ETag, entityTag); + response.Headers.Set(HttpHeaderNames.LastModified, HttpDate.Format(info.LastModifiedUtc)); + response.Headers.Set(HttpHeaderNames.CacheControl, "max-age=0, must-revalidate"); + response.Headers.Set(HttpHeaderNames.AcceptRanges, "bytes"); + } + + // Attempts to map a module-relative URL path to a mapped resource, + // handling DefaultDocument and DefaultExtension. + // Returns null if not found. + // Directories mus be returned regardless of directory listing being enabled. + private MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider) + { + var result = Provider.MapUrlPath(urlPath, mimeTypeProvider); + + // If urlPath maps to a file, no further searching is needed. + if (result?.IsFile ?? false) + return result; + + // Look for a default document. + // Don't append an additional slash if the URL path is "/". + // The default document, if found, must be a file, not a directory. + if (DefaultDocument != null) + { + var defaultDocumentPath = urlPath + (urlPath.Length > 1 ? "/" : string.Empty) + DefaultDocument; + var defaultDocumentResult = Provider.MapUrlPath(defaultDocumentPath, mimeTypeProvider); + if (defaultDocumentResult?.IsFile ?? false) + return defaultDocumentResult; + } + + // Try to apply default extension (but not if the URL path is "/", + // i.e. the only normalized, non-base URL path that ends in a slash). + // When the default extension is applied, the result must be a file. + if (DefaultExtension != null && urlPath.Length > 1) + { + var defaultExtensionResult = Provider.MapUrlPath(urlPath + DefaultExtension, mimeTypeProvider); + if (defaultExtensionResult?.IsFile ?? false) + return defaultExtensionResult; + } + + return result; + } + + private async Task HandleResource(IHttpContext context, MappedResourceInfo info, bool sendResponseBody) + { + // Try to extract resource information from cache. + var cachingThreshold = 1024L * Cache.MaxFileSizeKb; + if (!_cacheSection!.TryGet(info.Path, out var cacheItem)) + { + // Resource information not yet cached + cacheItem = new FileCacheItem(_cacheSection, info.LastModifiedUtc, info.Length); + _cacheSection.Add(info.Path, cacheItem); + } + else if (!Provider.IsImmutable) + { + // Check whether the resource has changed. + // If so, discard the cache item and create a new one. + if (cacheItem.LastModifiedUtc != info.LastModifiedUtc || cacheItem.Length != info.Length) + { + _cacheSection.Remove(info.Path); + cacheItem = new FileCacheItem(_cacheSection, info.LastModifiedUtc, info.Length); + _cacheSection.Add(info.Path, cacheItem); + } + } + + /* + * Now we have a cacheItem for the resource. + * It may have been just created, or it may or may not have a cached content, + * depending upon the value of the ContentCaching property, + * the size of the resource, and the value of the + * MaxFileSizeKb of our Cache. + */ + + // If the content type is not a valid MIME type, assume the default. + var contentType = info.ContentType ?? DirectoryLister?.ContentType ?? MimeType.Default; + var mimeType = MimeType.StripParameters(contentType); + if (!MimeType.IsMimeType(mimeType, false)) + contentType = mimeType = MimeType.Default; + + // Next we're going to apply proactive negotiation + // to determine whether we agree with the client upon the compression + // (or lack of it) to use for the resource. + // + // The combination of partial responses and entity compression + // is not really standardized and could lead to a world of pain. + // Thus, if there is a Range header in the request, try to negotiate for no compression. + // Later, if there is compression anyway, we will ignore the Range header. + if (!context.TryDetermineCompression(mimeType, out var preferCompression)) + preferCompression = true; + preferCompression &= context.Request.Headers.Get(HttpHeaderNames.Range) == null; + if (!context.Request.TryNegotiateContentEncoding(preferCompression, out var compressionMethod, out var setCompressionInResponse)) + { + // If negotiation failed, the returned callback will do the right thing. + setCompressionInResponse(context.Response); + return; + } + + var entityTag = info.GetEntityTag(compressionMethod); + + // Send a "304 Not Modified" response if applicable. + // + // RFC7232, Section 3.3: "A recipient MUST ignore If-Modified-Since + // if the request contains an If-None-Match header field." + if (context.Request.CheckIfNoneMatch(entityTag, out var ifNoneMatchExists) + || (!ifNoneMatchExists && context.Request.CheckIfModifiedSince(info.LastModifiedUtc, out _))) + { + context.Response.StatusCode = (int)HttpStatusCode.NotModified; + PreparePositiveResponse(context.Response, info, contentType, entityTag, setCompressionInResponse); + return; + } + + /* + * At this point we know the response is "200 OK", + * unless the request is a range request. + * + * RFC7233, Section 3.1: "The Range header field is evaluated after evaluating the precondition + * header fields defined in RFC7232, and only if the result in absence + * of the Range header field would be a 200 (OK) response. In other + * words, Range is ignored when a conditional GET would result in a 304 + * (Not Modified) response." + */ + + // Before evaluating ranges, we must know the content length. + // This is easy for files, as it is stored in info.Length. + // Directories always have info.Length == 0; therefore, + // unless the directory listing is cached, we must generate it now + // (and cache it while we're there, if applicable). + var content = cacheItem.GetContent(compressionMethod); + if (info.IsDirectory && content == null) + { + long uncompressedLength; + (content, uncompressedLength) = await GenerateDirectoryListingAsync(context, info, compressionMethod) + .ConfigureAwait(false); + if (ContentCaching && uncompressedLength <= cachingThreshold) + _ = cacheItem.SetContent(compressionMethod, content); + } + + var contentLength = content?.Length ?? info.Length; + + // Ignore range request is compression is enabled + // (or should I say forced, since negotiation has tried not to use it). + var partialStart = 0L; + var partialUpperBound = contentLength - 1; + var isPartial = compressionMethod == CompressionMethod.None + && context.Request.IsRangeRequest(contentLength, entityTag, info.LastModifiedUtc, out partialStart, out partialUpperBound); + var responseContentLength = contentLength; + + if (isPartial) + { + // Prepare a "206 Partial Content" response. + responseContentLength = partialUpperBound - partialStart + 1; + context.Response.StatusCode = (int)HttpStatusCode.PartialContent; + PreparePositiveResponse(context.Response, info, contentType, entityTag, setCompressionInResponse); + context.Response.Headers.Set(HttpHeaderNames.ContentRange, $"bytes {partialStart}-{partialUpperBound}/{contentLength}"); + } + else + { + // Prepare a "200 OK" response. + PreparePositiveResponse(context.Response, info, contentType, entityTag, setCompressionInResponse); + } + + // If it's a HEAD request, we're done. + if (!sendResponseBody) + return; + + // If content must be sent AND cached, first read it and store it. + // If the requested resource is a directory, we have already listed it by now, + // so it must be a file for content to be null. + if (content == null && ContentCaching && contentLength <= cachingThreshold) + { + using (var memoryStream = new MemoryStream()) + { + using (var compressor = new CompressionStream(memoryStream, compressionMethod)) + { + using var source = Provider.OpenFile(info.Path); + await source.CopyToAsync(compressor, WebServer.StreamCopyBufferSize, context.CancellationToken) + .ConfigureAwait(false); + } + + content = memoryStream.ToArray(); + responseContentLength = content.Length; + } + + _ = cacheItem.SetContent(compressionMethod, content); + } + + // Transfer cached content if present. + if (content != null) + { + context.Response.ContentLength64 = responseContentLength; + var offset = isPartial ? (int) partialStart : 0; + await context.Response.OutputStream.WriteAsync(content, offset, (int)responseContentLength, context.CancellationToken) + .ConfigureAwait(false); + + return; + } + + // Read and transfer content without caching. + using (var source = Provider.OpenFile(info.Path)) + { + context.Response.SendChunked = true; + + if (isPartial) + { + var buffer = new byte[WebServer.StreamCopyBufferSize]; + if (source.CanSeek) + { + source.Position = partialStart; + } + else + { + var skipLength = (int)partialStart; + while (skipLength > 0) + { + var read = await source.ReadAsync(buffer, 0, Math.Min(skipLength, buffer.Length), context.CancellationToken) + .ConfigureAwait(false); + + skipLength -= read; + } + } + + var transferSize = responseContentLength; + while (transferSize >= WebServer.StreamCopyBufferSize) + { + var read = await source.ReadAsync(buffer, 0, WebServer.StreamCopyBufferSize, context.CancellationToken) + .ConfigureAwait(false); + + await context.Response.OutputStream.WriteAsync(buffer, 0, read, context.CancellationToken) + .ConfigureAwait(false); + + transferSize -= read; + } + + if (transferSize > 0) + { + var read = await source.ReadAsync(buffer, 0, (int)transferSize, context.CancellationToken) + .ConfigureAwait(false); + + await context.Response.OutputStream.WriteAsync(buffer, 0, read, context.CancellationToken) + .ConfigureAwait(false); + } + } + else + { + using var compressor = new CompressionStream(context.Response.OutputStream, compressionMethod); + await source.CopyToAsync(compressor, WebServer.StreamCopyBufferSize, context.CancellationToken) + .ConfigureAwait(false); + } + } + } + + // Uses DirectoryLister to generate a directory listing asynchronously. + // Returns a tuple of the generated content and its *uncompressed* length + // (useful to decide whether it can be cached). + private async Task<(byte[], long)> GenerateDirectoryListingAsync( + IHttpContext context, + MappedResourceInfo info, + CompressionMethod compressionMethod) + { + using var memoryStream = new MemoryStream(); + using var stream = new CompressionStream(memoryStream, compressionMethod); + + await DirectoryLister!.ListDirectoryAsync( + info, + context.Request.Url.AbsolutePath, + Provider.GetDirectoryEntries(info.Path, context), + stream, + context.CancellationToken).ConfigureAwait(false); + + return (memoryStream.ToArray(), stream.UncompressedLength); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/FileModuleExtensions.cs b/Vendor/EmbedIO-3.5.2/Files/FileModuleExtensions.cs new file mode 100644 index 0000000..b315747 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/FileModuleExtensions.cs @@ -0,0 +1,282 @@ +using System; + +namespace EmbedIO.Files +{ + /// + /// Provides extension methods for and derived classes. + /// + public static class FileModuleExtensions + { + /// + /// Sets the used by a module to store hashes and, + /// optionally, file contents and rendered directory listings. + /// + /// The type of the module on which this method is called. + /// The module on which this method is called. + /// An instance of . + /// with its Cache property + /// set to . + /// is . + /// The configuration of is locked. + /// is . + /// + public static TModule WithCache(this TModule @this, FileCache value) + where TModule : FileModule + { + @this.Cache = value; + return @this; + } + + /// + /// Sets a value indicating whether a module caches the contents of files + /// and directory listings. + /// + /// The type of the module on which this method is called. + /// The module on which this method is called. + /// to enable caching of contents; + /// to disable it. + /// with its ContentCaching property + /// set to . + /// is . + /// The configuration of is locked. + /// + public static TModule WithContentCaching(this TModule @this, bool value) + where TModule : FileModule + { + @this.ContentCaching = value; + return @this; + } + + /// + /// Enables caching of file contents and directory listings on a module. + /// + /// The type of the module on which this method is called. + /// The module on which this method is called. + /// with its ContentCaching property + /// set to . + /// is . + /// The configuration of is locked. + /// + public static TModule WithContentCaching(this TModule @this) + where TModule : FileModule + { + @this.ContentCaching = true; + return @this; + } + + /// + /// Enables caching of file contents and directory listings on a module. + /// + /// The type of the module on which this method is called. + /// The module on which this method is called. + /// sets the maximum size of a single cached file in kilobytes + /// sets the maximum total size of cached data in kilobytes + /// with its ContentCaching property + /// set to . + /// is . + /// The configuration of is locked. + /// + public static TModule WithContentCaching(this TModule @this, int maxFileSizeKb, int maxSizeKb) + where TModule : FileModule + { + @this.ContentCaching = true; + @this.Cache.MaxFileSizeKb = maxFileSizeKb; + @this.Cache.MaxSizeKb = maxSizeKb; + return @this; + } + + /// + /// Disables caching of file contents and directory listings on a module. + /// + /// The type of the module on which this method is called. + /// The module on which this method is called. + /// with its ContentCaching property + /// set to . + /// is . + /// The configuration of is locked. + /// + public static TModule WithoutContentCaching(this TModule @this) + where TModule : FileModule + { + @this.ContentCaching = false; + return @this; + } + + /// + /// Sets the name of the default document served, if it exists, instead of a directory listing + /// when the path of a requested URL maps to a directory. + /// + /// The type of the module on which this method is called. + /// The module on which this method is called. + /// The name of the default document. + /// with its DefaultDocument property + /// set to . + /// is . + /// The configuration of is locked. + /// + public static TModule WithDefaultDocument(this TModule @this, string value) + where TModule : FileModule + { + @this.DefaultDocument = value; + return @this; + } + + /// + /// Sets the name of the default document to . + /// + /// The type of the module on which this method is called. + /// The module on which this method is called. + /// with its DefaultDocument property + /// set to . + /// is . + /// The configuration of is locked. + /// + public static TModule WithoutDefaultDocument(this TModule @this) + where TModule : FileModule + { + @this.DefaultDocument = null; + return @this; + } + + /// + /// Sets the default extension appended to requested URL paths that do not map + /// to any file or directory. + /// + /// The type of the module on which this method is called. + /// The module on which this method is called. + /// The default extension. + /// with its DefaultExtension property + /// set to . + /// is . + /// The configuration of is locked. + /// is a non-, + /// non-empty string that does not start with a period (.). + /// + public static TModule WithDefaultExtension(this TModule @this, string value) + where TModule : FileModule + { + @this.DefaultExtension = value; + return @this; + } + + /// + /// Sets the default extension to . + /// + /// The type of the module on which this method is called. + /// The module on which this method is called. + /// with its DefaultExtension property + /// set to . + /// is . + /// The configuration of is locked. + /// + public static TModule WithoutDefaultExtension(this TModule @this) + where TModule : FileModule + { + @this.DefaultExtension = null; + return @this; + } + + /// + /// Sets the interface used to generate + /// directory listing in a module. + /// + /// The type of the module on which this method is called. + /// The module on which this method is called. + /// An interface, or + /// to disable the generation of directory listings. + /// with its DirectoryLister property + /// set to . + /// is . + /// The configuration of is locked. + /// + public static TModule WithDirectoryLister(this TModule @this, IDirectoryLister value) + where TModule : FileModule + { + @this.DirectoryLister = value; + return @this; + } + + /// + /// Sets a module's DirectoryLister property + /// to , disabling the generation of directory listings. + /// + /// The type of the module on which this method is called. + /// The module on which this method is called. + /// with its DirectoryLister property + /// set to . + /// is . + /// The configuration of is locked. + /// + public static TModule WithoutDirectoryLister(this TModule @this) + where TModule : FileModule + { + @this.DirectoryLister = null; + return @this; + } + + /// + /// Sets a that is called by a module whenever + /// the requested URL path could not be mapped to any file or directory. + /// + /// The type of the module on which this method is called. + /// The module on which this method is called. + /// The method to call. + /// with its OnMappingFailed property + /// set to . + /// is . + /// The configuration of is locked. + /// is . + /// + /// + public static TModule HandleMappingFailed(this TModule @this, FileRequestHandlerCallback callback) + where TModule : FileModule + { + @this.OnMappingFailed = callback; + return @this; + } + + /// + /// Sets a that is called by a module whenever + /// the requested URL path has been mapped to a directory, but directory listing has been + /// disabled. + /// + /// The type of the module on which this method is called. + /// The module on which this method is called. + /// The method to call. + /// with its OnDirectoryNotListable property + /// set to . + /// is . + /// The configuration of is locked. + /// is . + /// + /// + public static TModule HandleDirectoryNotListable(this TModule @this, FileRequestHandlerCallback callback) + where TModule : FileModule + { + @this.OnDirectoryNotListable = callback; + return @this; + } + + /// + /// Sets a that is called by a module whenever + /// the requested URL path has been mapped to a file or directory, but the request's + /// HTTP method is neither GET nor HEAD. + /// + /// The type of the module on which this method is called. + /// The module on which this method is called. + /// The method to call. + /// with its OnMethodNotAllowed property + /// set to . + /// is . + /// The configuration of is locked. + /// is . + /// + /// + public static TModule HandleMethodNotAllowed(this TModule @this, FileRequestHandlerCallback callback) + where TModule : FileModule + { + @this.OnMethodNotAllowed = callback; + return @this; + } + } +} diff --git a/Vendor/EmbedIO-3.5.2/Files/FileRequestHandler.cs b/Vendor/EmbedIO-3.5.2/Files/FileRequestHandler.cs new file mode 100644 index 0000000..805917c --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/FileRequestHandler.cs @@ -0,0 +1,53 @@ +using System.Threading.Tasks; + +namespace EmbedIO.Files +{ + /// + /// Provides standard handler callbacks for . + /// + /// + public static class FileRequestHandler + { +#pragma warning disable CA1801 // Unused parameters - Must respect FileRequestHandlerCallback signature. + /// + /// Unconditionally passes a request down the module chain. + /// + /// An interface representing the context of the request. + /// If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping; + /// otherwise, . + /// This method never returns; it throws an exception instead. + public static Task PassThrough(IHttpContext context, MappedResourceInfo? info) + => throw RequestHandler.PassThrough(); + + /// + /// Unconditionally sends a 403 Unauthorized response. + /// + /// An interface representing the context of the request. + /// If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping; + /// otherwise, . + /// This method never returns; it throws a instead. + public static Task ThrowUnauthorized(IHttpContext context, MappedResourceInfo? info) + => throw HttpException.Unauthorized(); + + /// + /// Unconditionally sends a 404 Not Found response. + /// + /// An interface representing the context of the request. + /// If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping; + /// otherwise, . + /// This method never returns; it throws a instead. + public static Task ThrowNotFound(IHttpContext context, MappedResourceInfo? info) + => throw HttpException.NotFound(); + + /// + /// Unconditionally sends a 405 Method Not Allowed response. + /// + /// An interface representing the context of the request. + /// If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping; + /// otherwise, . + /// This method never returns; it throws a instead. + public static Task ThrowMethodNotAllowed(IHttpContext context, MappedResourceInfo? info) + => throw HttpException.MethodNotAllowed(); +#pragma warning restore CA1801 + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/FileRequestHandlerCallback.cs b/Vendor/EmbedIO-3.5.2/Files/FileRequestHandlerCallback.cs new file mode 100644 index 0000000..0340e14 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/FileRequestHandlerCallback.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace EmbedIO.Files +{ + /// + /// A callback used to handle a request in . + /// + /// An interface representing the context of the request. + /// If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping; + /// otherwise, . + /// A representing the ongoing operation. + public delegate Task FileRequestHandlerCallback(IHttpContext context, MappedResourceInfo? info); +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/FileSystemProvider.cs b/Vendor/EmbedIO-3.5.2/Files/FileSystemProvider.cs new file mode 100644 index 0000000..26f8f21 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/FileSystemProvider.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using EmbedIO.Utilities; + +namespace EmbedIO.Files +{ + /// + /// Provides access to the local file system to a . + /// + /// + public class FileSystemProvider : IDisposable, IFileProvider + { + private readonly FileSystemWatcher? _watcher; + + /// + /// Initializes a new instance of the class. + /// + /// + /// OSX doesn't support , the parameter will be always . + /// + /// The file system path. + /// if files and directories in + /// are not expected to change during a web server's + /// lifetime; otherwise. + /// is . + /// is not a valid local path. + /// + public FileSystemProvider(string fileSystemPath, bool isImmutable) + { + FileSystemPath = Validate.LocalPath(nameof(fileSystemPath), fileSystemPath, true); + IsImmutable = isImmutable || RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + + try + { + if (!IsImmutable) + _watcher = new FileSystemWatcher(FileSystemPath); + } + catch (PlatformNotSupportedException) + { + IsImmutable = true; + } + } + + /// + /// Finalizes an instance of the class. + /// + ~FileSystemProvider() + { + Dispose(false); + } + + /// + public event Action? ResourceChanged; + + /// + /// Gets the file system path from which files are retrieved. + /// + public string FileSystemPath { get; } + + /// + public bool IsImmutable { get; } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public void Start(CancellationToken cancellationToken) + { + if (_watcher != null) + { + _watcher.Changed += Watcher_ChangedOrDeleted; + _watcher.Deleted += Watcher_ChangedOrDeleted; + _watcher.Renamed += Watcher_Renamed; + _watcher.EnableRaisingEvents = true; + } + } + + /// + public MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider) + { + urlPath = urlPath.Substring(1); // Drop the initial slash + string localPath; + + // Disable CA1031 as there's little we can do if IsPathRooted or GetFullPath fails. +#pragma warning disable CA1031 + try + { + // Unescape the url before continue + urlPath = Uri.UnescapeDataString(urlPath); + + // Bail out early if the path is a rooted path, + // as Path.Combine would ignore our base path. + // See https://docs.microsoft.com/en-us/dotnet/api/system.io.path.combine + // (particularly the Remarks section). + // + // Under Windows, a relative URL path may be a full filesystem path + // (e.g. "D:\foo\bar" or "\\192.168.0.1\Shared\MyDocuments\BankAccounts.docx"). + // Under Unix-like operating systems we have no such problems, as relativeUrlPath + // can never start with a slash; however, loading one more class from Swan + // just to check the OS type would probably outweigh calling IsPathRooted. + if (Path.IsPathRooted(urlPath)) + return null; + + // Convert the relative URL path to a relative filesystem path + // (practically a no-op under Unix-like operating systems) + // and combine it with our base local path to obtain a full path. + localPath = Path.Combine(FileSystemPath, urlPath.Replace('/', Path.DirectorySeparatorChar)); + + // Use GetFullPath as an additional safety check + // for relative paths that contain a rooted path + // (e.g. "valid/path/C:\Windows\System.ini") + localPath = Path.GetFullPath(localPath); + } + catch + { + // Both IsPathRooted and GetFullPath throw exceptions + // if a path contains invalid characters or is otherwise invalid; + // bail out in this case too, as the path would not exist on disk anyway. + return null; + } +#pragma warning restore CA1031 + + // As a final precaution, check that the resulting local path + // is inside the folder intended to be served. + if (!localPath.StartsWith(FileSystemPath, StringComparison.Ordinal)) + return null; + + if (File.Exists(localPath)) + return GetMappedFileInfo(mimeTypeProvider, localPath); + + if (Directory.Exists(localPath)) + return GetMappedDirectoryInfo(localPath); + + return null; + } + + /// + public Stream OpenFile(string path) => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + + /// + public IEnumerable GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider) + => new DirectoryInfo(path).EnumerateFileSystemInfos() + .Select(fsi => GetMappedResourceInfo(mimeTypeProvider, fsi)); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + ResourceChanged = null; // Release references to listeners + + if (_watcher != null) + { + _watcher.EnableRaisingEvents = false; + _watcher.Changed -= Watcher_ChangedOrDeleted; + _watcher.Deleted -= Watcher_ChangedOrDeleted; + _watcher.Renamed -= Watcher_Renamed; + + if (disposing) + _watcher.Dispose(); + } + } + + private static MappedResourceInfo GetMappedFileInfo(IMimeTypeProvider mimeTypeProvider, string localPath) + => GetMappedFileInfo(mimeTypeProvider, new FileInfo(localPath)); + + private static MappedResourceInfo GetMappedFileInfo(IMimeTypeProvider mimeTypeProvider, FileInfo info) + => MappedResourceInfo.ForFile( + info.FullName, + info.Name, + info.LastWriteTimeUtc, + info.Length, + mimeTypeProvider.GetMimeType(info.Extension)); + + private static MappedResourceInfo GetMappedDirectoryInfo(string localPath) + => GetMappedDirectoryInfo(new DirectoryInfo(localPath)); + + private static MappedResourceInfo GetMappedDirectoryInfo(DirectoryInfo info) + => MappedResourceInfo.ForDirectory(info.FullName, info.Name, info.LastWriteTimeUtc); + + private static MappedResourceInfo GetMappedResourceInfo(IMimeTypeProvider mimeTypeProvider, FileSystemInfo info) + => info is DirectoryInfo directoryInfo + ? GetMappedDirectoryInfo(directoryInfo) + : GetMappedFileInfo(mimeTypeProvider, (FileInfo) info); + + private void Watcher_ChangedOrDeleted(object sender, FileSystemEventArgs e) + => ResourceChanged?.Invoke(e.FullPath); + + private void Watcher_Renamed(object sender, RenamedEventArgs e) + => ResourceChanged?.Invoke(e.OldFullPath); + } +} diff --git a/Vendor/EmbedIO-3.5.2/Files/IDirectoryLister.cs b/Vendor/EmbedIO-3.5.2/Files/IDirectoryLister.cs new file mode 100644 index 0000000..0a2ba9a --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/IDirectoryLister.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace EmbedIO.Files +{ + /// + /// Represents an object that can render a directory listing to a stream. + /// + public interface IDirectoryLister + { + /// + /// Gets the MIME type of generated directory listings. + /// + string ContentType { get; } + + /// + /// Asynchronously generate a directory listing. + /// + /// A containing information about + /// the directory which is to be listed. + /// The absolute URL path that was mapped to . + /// An enumeration of the entries in the directory represented by . + /// A to which the directory listing must be written. + /// A used to cancel the operation. + /// A representing the ongoing operation. + Task ListDirectoryAsync( + MappedResourceInfo info, + string absoluteUrlPath, + IEnumerable entries, + Stream stream, + CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/IFileProvider.cs b/Vendor/EmbedIO-3.5.2/Files/IFileProvider.cs new file mode 100644 index 0000000..048265c --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/IFileProvider.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace EmbedIO.Files +{ + /// + /// Represents an object that can provide files and/or directories to be served by a . + /// + public interface IFileProvider + { + /// + /// Occurs when a file or directory provided by this instance is modified or removed. + /// The event's parameter is the provider-specific path of the resource that changed. + /// + event Action ResourceChanged; + + /// + /// Gets a value indicating whether the files and directories provided by this instance + /// will never change. + /// + bool IsImmutable { get; } + + /// + /// Signals a file provider that the web server is starting. + /// + /// A used to stop the web server. + void Start(CancellationToken cancellationToken); + + /// + /// Maps a URL path to a provider-specific path. + /// + /// The URL path. + /// An interface to use + /// for determining the MIME type of a file. + /// A provider-specific path identifying a file or directory, + /// or if this instance cannot provide a resource associated + /// to . + MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider); + + /// + /// Opens a file for reading. + /// + /// The provider-specific path for the file. + /// + /// A readable of the file's contents. + /// + Stream OpenFile(string path); + + /// + /// Returns an enumeration of the entries of a directory. + /// + /// The provider-specific path for the directory. + /// An interface to use + /// for determining the MIME type of files. + /// An enumeration of objects identifying the entries + /// in the directory identified by . + IEnumerable GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/Internal/Base64Utility.cs b/Vendor/EmbedIO-3.5.2/Files/Internal/Base64Utility.cs new file mode 100644 index 0000000..19d1579 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/Internal/Base64Utility.cs @@ -0,0 +1,12 @@ +using System; + +namespace EmbedIO.Files.Internal +{ + internal static class Base64Utility + { + // long is 8 bytes + // base64 of 8 bytes is 12 chars, but the last one is padding + public static string LongToBase64(long value) + => Convert.ToBase64String(BitConverter.GetBytes(value)).Substring(0, 11); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/Internal/EntityTag.cs b/Vendor/EmbedIO-3.5.2/Files/Internal/EntityTag.cs new file mode 100644 index 0000000..288d594 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/Internal/EntityTag.cs @@ -0,0 +1,28 @@ +using System; +using System.Text; + +namespace EmbedIO.Files.Internal +{ + internal static class EntityTag + { + public static string Compute(DateTime lastModifiedUtc, long length, CompressionMethod compressionMethod) + { + var sb = new StringBuilder() + .Append('"') + .Append(Base64Utility.LongToBase64(lastModifiedUtc.Ticks)) + .Append(Base64Utility.LongToBase64(length)); + + switch (compressionMethod) + { + case CompressionMethod.Deflate: + sb.Append('-').Append(CompressionMethodNames.Deflate); + break; + case CompressionMethod.Gzip: + sb.Append('-').Append(CompressionMethodNames.Gzip); + break; + } + + return sb.Append('"').ToString(); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/Internal/FileCacheItem.cs b/Vendor/EmbedIO-3.5.2/Files/Internal/FileCacheItem.cs new file mode 100644 index 0000000..78aa18e --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/Internal/FileCacheItem.cs @@ -0,0 +1,164 @@ +using System; +using EmbedIO.Internal; + +namespace EmbedIO.Files.Internal +{ + internal sealed class FileCacheItem + { +#pragma warning disable SA1401 // Field should be private - performance is a stronger concern here. + // These fields create a sort of linked list of items + // inside the cache's dictionary. + // Their purpose is to keep track of items + // in order from least to most recently used. + internal string? PreviousKey; + internal string? NextKey; + internal long LastUsedAt; +#pragma warning restore SA1401 + + // Size of a pointer in bytes + private static readonly long SizeOfPointer = Environment.Is64BitProcess ? 8 : 4; + + // Size of a WeakReference in bytes + private static readonly long SizeOfWeakReference = Environment.Is64BitProcess ? 16 : 32; + + // Educated guess about the size of an Item in memory (see comments on constructor). + // 3 * SizeOfPointer + total size of fields, rounded up to a multiple of 16. + // + // Computed as follows: + // + // * for 32-bit: + // - initialize count to 3 (number of "hidden" pointers that compose the object header) + // - for every field / auto property, in order of declaration: + // - increment count by 1 for reference types, 2 for long and DateTime + // (as of time of writing there are no fields of other types here) + // - increment again by 1 if this field "weighs" 1 and the next one "weighs" 2 + // (padding for field alignment) + // - multiply count by 4 (size of a pointer) + // - if the result is not a multiple of 16, round it up to next multiple of 16 + // + // * for 64-bit: + // - initialize count to 3 (number of "hidden" pointers that compose the object header) + // - for every field / auto property, in order of declaration, increment count by 1 + // (at the time of writing there are no fields here that need padding on 64-bit) + // - multiply count by 8 (size of a pointer) + // - if the result is not a multiple of 16, round it up to next multiple of 16 + private static readonly long SizeOfItem = Environment.Is64BitProcess ? 96 : 128; + + private readonly object _syncRoot = new object(); + + // Used to update total size of section. + // Weak reference avoids circularity. + private readonly WeakReference _section; + + // There are only 3 possible compression methods, + // hence a dictionary (or two dictionaries) would be overkill. + private byte[]? _uncompressedContent; + private byte[]? _gzippedContent; + private byte[]? _deflatedContent; + + internal FileCacheItem(FileCache.Section section, DateTime lastModifiedUtc, long length) + { + _section = new WeakReference(section); + + LastModifiedUtc = lastModifiedUtc; + Length = length; + + // There is no way to know the actual size of an object at runtime. + // This method makes some educated guesses, based on the following + // article (among others): + // https://codingsight.com/precise-computation-of-clr-object-size/ + // PreviousKey and NextKey values aren't counted in + // because they are just references to existing strings. + SizeInCache = SizeOfItem + SizeOfWeakReference; + } + + public DateTime LastModifiedUtc { get; } + + public long Length { get; } + + // This is the (approximate) in-memory size of this object. + // It is NOT the length of the cache resource! + public long SizeInCache { get; private set; } + + public byte[]? GetContent(CompressionMethod compressionMethod) + { + // If there are both entity tag and content, use them. + switch (compressionMethod) + { + case CompressionMethod.Deflate: + if (_deflatedContent != null) return _deflatedContent; + break; + case CompressionMethod.Gzip: + if (_gzippedContent != null) return _gzippedContent; + break; + default: + if (_uncompressedContent != null) return _uncompressedContent; + break; + } + + // Try to convert existing content, if any. + byte[]? content; + if (_uncompressedContent != null) + { + content = CompressionUtility.ConvertCompression(_uncompressedContent, CompressionMethod.None, compressionMethod); + } + else if (_gzippedContent != null) + { + content = CompressionUtility.ConvertCompression(_gzippedContent, CompressionMethod.Gzip, compressionMethod); + } + else if (_deflatedContent != null) + { + content = CompressionUtility.ConvertCompression(_deflatedContent, CompressionMethod.Deflate, compressionMethod); + } + else + { + // No content whatsoever. + return null; + } + + return SetContent(compressionMethod, content); + } + + public byte[]? SetContent(CompressionMethod compressionMethod, byte[]? content) + { + // This is the bare minimum locking we need + // to ensure we don't mess sizes up. + byte[]? oldContent; + lock (_syncRoot) + { + switch (compressionMethod) + { + case CompressionMethod.Deflate: + oldContent = _deflatedContent; + _deflatedContent = content; + break; + case CompressionMethod.Gzip: + oldContent = _gzippedContent; + _gzippedContent = content; + break; + default: + oldContent = _uncompressedContent; + _uncompressedContent = content; + break; + } + } + + var sizeDelta = GetSizeOf(content) - GetSizeOf(oldContent); + SizeInCache += sizeDelta; + if (_section.TryGetTarget(out var section)) + section.UpdateTotalSize(sizeDelta); + + return content; + } + + // Round up to a multiple of 16 + private static long RoundUpTo16(long n) + { + var remainder = n % 16; + return remainder > 0 ? n + (16 - remainder) : n; + } + + // The size of a byte array is 3 * SizeOfPointer + 1 (size of byte) * Length + private static long GetSizeOf(byte[]? arr) => arr == null ? 0 : RoundUpTo16(3 * SizeOfPointer) + arr.Length; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/Internal/HtmlDirectoryLister.cs b/Vendor/EmbedIO-3.5.2/Files/Internal/HtmlDirectoryLister.cs new file mode 100644 index 0000000..f85eebf --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/Internal/HtmlDirectoryLister.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Utilities; +using Swan; + +namespace EmbedIO.Files.Internal +{ + internal class HtmlDirectoryLister : IDirectoryLister + { + private static readonly Lazy LazyInstance = new Lazy(() => new HtmlDirectoryLister()); + + private HtmlDirectoryLister() + { + } + + public static IDirectoryLister Instance => LazyInstance.Value; + + public string ContentType { get; } = MimeType.Html + "; encoding=" + WebServer.DefaultEncoding.WebName; + + public async Task ListDirectoryAsync( + MappedResourceInfo info, + string absoluteUrlPath, + IEnumerable entries, + Stream stream, + CancellationToken cancellationToken) + { + const int MaxEntryLength = 50; + const int SizeIndent = -20; // Negative for right alignment + + if (!info.IsDirectory) + throw SelfCheck.Failure($"{nameof(HtmlDirectoryLister)}.{nameof(ListDirectoryAsync)} invoked with a file, not a directory."); + + var encodedPath = WebUtility.HtmlEncode(absoluteUrlPath); + using var text = new StreamWriter(stream, WebServer.DefaultEncoding); + text.Write("Index of "); + text.Write(encodedPath); + text.Write("

Index of "); + text.Write(encodedPath); + text.Write("


");
+
+            if (encodedPath.Length > 1)
+                text.Write("../\n");
+
+            entries = entries.ToArray();
+
+            foreach (var directory in entries.Where(m => m.IsDirectory).OrderBy(e => e.Name))
+            {
+                text.Write($"{WebUtility.HtmlEncode(directory.Name)}");
+                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($"{WebUtility.HtmlEncode(file.Name)}");
+                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("

"); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/Internal/MappedResourceInfoExtensions.cs b/Vendor/EmbedIO-3.5.2/Files/Internal/MappedResourceInfoExtensions.cs new file mode 100644 index 0000000..4d780d4 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/Internal/MappedResourceInfoExtensions.cs @@ -0,0 +1,8 @@ +namespace EmbedIO.Files.Internal +{ + internal static class MappedResourceInfoExtensions + { + public static string GetEntityTag(this MappedResourceInfo @this, CompressionMethod compressionMethod) + => EntityTag.Compute(@this.LastModifiedUtc, @this.Length, compressionMethod); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/MappedResourceInfo.cs b/Vendor/EmbedIO-3.5.2/Files/MappedResourceInfo.cs new file mode 100644 index 0000000..914bc7a --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/MappedResourceInfo.cs @@ -0,0 +1,80 @@ +using System; + +namespace EmbedIO.Files +{ + /// + /// Contains information about a resource served via an . + /// + public sealed class MappedResourceInfo + { + private MappedResourceInfo(string path, string name, DateTime lastModifiedUtc, long length, string? contentType) + { + Path = path; + Name = name; + LastModifiedUtc = lastModifiedUtc; + Length = length; + ContentType = contentType; + } + + /// + /// Gets a value indicating whether this instance represents a directory. + /// + public bool IsDirectory => ContentType == null; + + /// + /// Gets a value indicating whether this instance represents a file. + /// + public bool IsFile => ContentType != null; + + /// + /// Gets a unique, provider-specific path for the resource. + /// + public string Path { get; } + + /// + /// Gets the name of the resource, as it would appear in a directory listing. + /// + public string Name { get; } + + /// + /// Gets the UTC date and time of the last modification made to the resource. + /// + public DateTime LastModifiedUtc { get; } + + /// + /// If is , gets the length of the file, expressed in bytes. + /// If is , this property is always zero. + /// + public long Length { get; } + + /// + /// If is , gets a MIME type describing the kind of contents of the file. + /// If is , this property is always . + /// + public string? ContentType { get; } + + /// + /// Creates and returns a new instance of the class, + /// representing a file. + /// + /// A unique, provider-specific path for the file. + /// The name of the file, as it would appear in a directory listing. + /// The UTC date and time of the last modification made to the file. + /// The length of the file, expressed in bytes. + /// A MIME type describing the kind of contents of the file. + /// A newly-constructed instance of . + public static MappedResourceInfo ForFile(string path, string name, DateTime lastModifiedUtc, long size, string contentType) + => new MappedResourceInfo(path, name, lastModifiedUtc, size, contentType ?? MimeType.Default); + + /// + /// Creates and returns a new instance of the class, + /// representing a directory. + /// + /// A unique, provider-specific path for the directory. + /// The name of the directory, as it would appear in a directory listing. + /// The UTC date and time of the last modification made to the directory. + /// A newly-constructed instance of . + public static MappedResourceInfo ForDirectory(string path, string name, DateTime lastModifiedUtc) + => new MappedResourceInfo(path, name, lastModifiedUtc, 0, null); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/ResourceFileProvider.cs b/Vendor/EmbedIO-3.5.2/Files/ResourceFileProvider.cs new file mode 100644 index 0000000..21e850a --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/ResourceFileProvider.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using EmbedIO.Utilities; + +namespace EmbedIO.Files +{ + /// + /// Provides access to embedded resources to a . + /// + /// + public class ResourceFileProvider : IFileProvider + { + private readonly DateTime _fileTime = DateTime.UtcNow; + + /// + /// Initializes a new instance of the class. + /// + /// The assembly where served files are contained as embedded resources. + /// A string to prepend to provider-specific paths + /// to form the name of a manifest resource in . + /// is . + public ResourceFileProvider(Assembly assembly, string pathPrefix) + { + Assembly = Validate.NotNull(nameof(assembly), assembly); + PathPrefix = pathPrefix ?? string.Empty; + } + + /// + public event Action ResourceChanged + { + add { } + remove { } + } + + /// + /// Gets the assembly where served files are contained as embedded resources. + /// + public Assembly Assembly { get; } + + /// + /// Gets a string that is prepended to provider-specific paths to form the name of a manifest resource in . + /// + public string PathPrefix { get; } + + /// + public bool IsImmutable => true; + + /// + public void Start(CancellationToken cancellationToken) + { + } + + /// + public MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider) + { + var resourceName = PathPrefix + urlPath.Replace('/', '.'); + + long size; + try + { + using var stream = Assembly.GetManifestResourceStream(resourceName); + if (stream == null || stream == Stream.Null) + return null; + + size = stream.Length; + } + catch (FileNotFoundException) + { + return null; + } + + var lastSlashPos = urlPath.LastIndexOf('/'); + var name = urlPath.Substring(lastSlashPos + 1); + + return MappedResourceInfo.ForFile( + resourceName, + name, + _fileTime, + size, + mimeTypeProvider.GetMimeType(Path.GetExtension(name))); + } + + /// + public Stream OpenFile(string path) => Assembly.GetManifestResourceStream(path); + + /// + public IEnumerable GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider) + => Enumerable.Empty(); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Files/ZipFileProvider.cs b/Vendor/EmbedIO-3.5.2/Files/ZipFileProvider.cs new file mode 100644 index 0000000..5a9c82d --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Files/ZipFileProvider.cs @@ -0,0 +1,110 @@ +using EmbedIO.Utilities; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading; + +namespace EmbedIO.Files +{ + /// + /// Provides access to files contained in a .zip file to a . + /// + /// + public class ZipFileProvider : IDisposable, IFileProvider + { + private readonly ZipArchive _zipArchive; + + /// + /// Initializes a new instance of the class. + /// + /// The zip file path. + public ZipFileProvider(string zipFilePath) + : this(new FileStream(Validate.LocalPath(nameof(zipFilePath), zipFilePath, true), FileMode.Open)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The stream that contains the archive. + /// to leave the stream open after the web server + /// is disposed; otherwise, . + public ZipFileProvider(Stream stream, bool leaveOpen = false) + { + _zipArchive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen); + } + + /// + /// Finalizes an instance of the class. + /// + ~ZipFileProvider() + { + Dispose(false); + } + + /// + public event Action ResourceChanged + { + add { } + remove { } + } + + /// + public bool IsImmutable => true; + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public void Start(CancellationToken cancellationToken) + { + } + + /// + public MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider) + { + if (urlPath.Length == 1) + return null; + + urlPath = Uri.UnescapeDataString(urlPath); + + var entry = _zipArchive.GetEntry(urlPath.Substring(1)); + if (entry == null) + return null; + + return MappedResourceInfo.ForFile( + entry.FullName, + entry.Name, + entry.LastWriteTime.DateTime, + entry.Length, + mimeTypeProvider.GetMimeType(Path.GetExtension(entry.Name))); + } + + /// + public Stream OpenFile(string path) + => _zipArchive.GetEntry(path)?.Open() ?? throw new FileNotFoundException($"\"{path}\" cannot be found in Zip archive."); + + /// + public IEnumerable GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider) + => Enumerable.Empty(); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!disposing) + return; + + _zipArchive.Dispose(); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Items.cs b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Items.cs new file mode 100644 index 0000000..d82f88f --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Items.cs @@ -0,0 +1,47 @@ +using System; + +namespace EmbedIO +{ + partial class HttpContextExtensions + { + /// Gets the item associated with the specified key. + /// The desired type of the item. + /// The on which this method is called. + /// The key whose value to get from the Items dictionary. + /// + /// When this method returns, the item associated with the specified key, + /// if the key is found in Items + /// and the associated value is of type ; + /// otherwise, the default value for . + /// This parameter is passed uninitialized. + /// + /// if the item is found and is of type ; + /// otherwise, . + /// is . + /// is . + public static bool TryGetItem(this IHttpContext @this, object key, out T value) + { + if (@this.Items.TryGetValue(key, out var item) && item is T typedItem) + { + value = typedItem; + return true; + } + +#pragma warning disable CS8653 // value is non-nullable - We are returning false, so value is undefined. + value = default; +#pragma warning restore CS8653 + return false; + } + + /// Gets the item associated with the specified key. + /// The desired type of the item. + /// The on which this method is called. + /// The key whose value to get from the Items dictionary. + /// The item associated with the specified key, + /// if the key is found in Items + /// and the associated value is of type ; + /// otherwise, the default value for . + public static T GetItem(this IHttpContext @this, object key) + => @this.Items.TryGetValue(key, out var item) && item is T typedItem ? typedItem : default; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Redirect.cs b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Redirect.cs new file mode 100644 index 0000000..5cc7ff9 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Redirect.cs @@ -0,0 +1,33 @@ +using System; +using System.Net; +using EmbedIO.Utilities; + +namespace EmbedIO +{ + partial class HttpContextExtensions + { + /// + /// Sets a redirection status code and adds a Location header to the response. + /// + /// The interface on which this method is called. + /// The URL to which the user agent should be redirected. + /// The status code to set on the response. + /// is . + /// is . + /// + /// is not a valid relative or absolute URL.. + /// - or - + /// is not a redirection (3xx) status code. + /// + public static void Redirect(this IHttpContext @this, string location, int statusCode = (int)HttpStatusCode.Found) + { + location = Validate.Url(nameof(location), location, @this.Request.Url); + + if (statusCode < 300 || statusCode > 399) + throw new ArgumentException("Redirect status code is not valid.", nameof(statusCode)); + + @this.Response.SetEmptyResponse(statusCode); + @this.Response.Headers[HttpHeaderNames.Location] = location; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpContextExtensions-RequestStream.cs b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-RequestStream.cs new file mode 100644 index 0000000..9f9158e --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-RequestStream.cs @@ -0,0 +1,61 @@ +using System.IO; +using System.IO.Compression; +using System.Text; +using Swan.Logging; + +namespace EmbedIO +{ + partial class HttpContextExtensions + { + /// + /// Wraps the request input stream and returns a that can be used directly. + /// Decompression of compressed request bodies is implemented if specified in the web server's options. + /// + /// The on which this method is called. + /// + /// A that can be used to write response data. + /// This stream MUST be disposed when finished writing. + /// + /// + /// + public static Stream OpenRequestStream(this IHttpContext @this) + { + var stream = @this.Request.InputStream; + + var encoding = @this.Request.Headers[HttpHeaderNames.ContentEncoding]?.Trim(); + switch (encoding) + { + case CompressionMethodNames.Gzip: + if (@this.SupportCompressedRequests) + return new GZipStream(stream, CompressionMode.Decompress); + break; + case CompressionMethodNames.Deflate: + if (@this.SupportCompressedRequests) + return new DeflateStream(stream, CompressionMode.Decompress); + break; + case CompressionMethodNames.None: + case null: + return stream; + } + + $"[{@this.Id}] Unsupported request content encoding \"{encoding}\", sending 400 Bad Request..." + .Warn(nameof(OpenRequestStream)); + + throw HttpException.BadRequest($"Unsupported content encoding \"{encoding}\""); + } + + /// + /// Wraps the request input stream and returns a that can be used directly. + /// Decompression of compressed request bodies is implemented if specified in the web server's options. + /// + /// The on which this method is called. + /// + /// A that can be used to read the request body as text. + /// This reader MUST be disposed when finished reading. + /// + /// + /// + public static TextReader OpenRequestText(this IHttpContext @this) + => new StreamReader(OpenRequestStream(@this), @this.Request.ContentEncoding); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Requests.cs b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Requests.cs new file mode 100644 index 0000000..f3948ee --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Requests.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Threading.Tasks; +using EmbedIO.Utilities; +using Swan; + +namespace EmbedIO +{ + partial class HttpContextExtensions + { + private static readonly object FormDataKey = new object(); + private static readonly object QueryDataKey = new object(); + + /// + /// Asynchronously retrieves the request body as an array of s. + /// + /// The on which this method is called. + /// A Task, representing the ongoing operation, + /// whose result will be an array of s containing the request body. + /// is . + public static async Task GetRequestBodyAsByteArrayAsync(this IHttpContext @this) + { + using var buffer = new MemoryStream(); + using var stream = @this.OpenRequestStream(); + await stream.CopyToAsync(buffer, WebServer.StreamCopyBufferSize, @this.CancellationToken).ConfigureAwait(false); + return buffer.ToArray(); + } + + /// + /// Asynchronously buffers the request body into a read-only . + /// + /// The on which this method is called. + /// A Task, representing the ongoing operation, + /// whose result will be a read-only containing the request body. + /// is . + public static async Task GetRequestBodyAsMemoryStreamAsync(this IHttpContext @this) + => new MemoryStream( + await GetRequestBodyAsByteArrayAsync(@this).ConfigureAwait(false), + false); + + /// + /// Asynchronously retrieves the request body as a string. + /// + /// The on which this method is called. + /// A Task, representing the ongoing operation, + /// whose result will be a representation of the request body. + /// is . + public static async Task GetRequestBodyAsStringAsync(this IHttpContext @this) + { + using var reader = @this.OpenRequestText(); + return await reader.ReadToEndAsync().ConfigureAwait(false); + } + + /// + /// Asynchronously deserializes a request body, using the default request deserializer. + /// As of EmbedIO version 3.0, the default response serializer has the same behavior of JSON + /// request parsing methods of version 2. + /// + /// The expected type of the deserialized data. + /// The on which this method is called. + /// A Task, representing the ongoing operation, + /// whose result will be the deserialized data. + /// is . + public static Task GetRequestDataAsync(this IHttpContext @this) + => RequestDeserializer.Default(@this); + + /// + /// Asynchronously deserializes a request body, using the specified request deserializer. + /// + /// The expected type of the deserialized data. + /// The on which this method is called. + /// A used to deserialize the request body. + /// A Task, representing the ongoing operation, + /// whose result will be the deserialized data. + /// is . + /// is . + public static Task GetRequestDataAsync(this IHttpContext @this,RequestDeserializerCallback deserializer) + => Validate.NotNull(nameof(deserializer), deserializer)(@this); + + /// + /// Asynchronously parses a request body in application/x-www-form-urlencoded format. + /// + /// The on which this method is called. + /// A Task, representing the ongoing operation, + /// whose result will be a read-only of form field names and values. + /// is . + /// + /// This method may safely be called more than once for the same : + /// it will return the same collection instead of trying to parse the request body again. + /// + public static async Task GetRequestFormDataAsync(this IHttpContext @this) + { + if (!@this.Items.TryGetValue(FormDataKey, out var previousResult)) + { + NameValueCollection result; + try + { + using var reader = @this.OpenRequestText(); + result = UrlEncodedDataParser.Parse(await reader.ReadToEndAsync().ConfigureAwait(false), false); + } + catch (Exception e) + { + @this.Items[FormDataKey] = e; + throw; + } + + @this.Items[FormDataKey] = result; + return result; + } + + switch (previousResult) + { + case NameValueCollection collection: + return collection; + + case Exception exception: + throw exception.RethrowPreservingStackTrace(); + + case null: + throw SelfCheck.Failure($"Previous result of {nameof(HttpContextExtensions)}.{nameof(GetRequestFormDataAsync)} is null."); + + default: + throw SelfCheck.Failure($"Previous result of {nameof(HttpContextExtensions)}.{nameof(GetRequestFormDataAsync)} is of unexpected type {previousResult.GetType().FullName}"); + } + } + + /// + /// Parses a request URL query. Note that this is different from getting the property, + /// in that fields without an equal sign are treated as if they have an empty value, instead of their keys being grouped + /// as values of the null key. + /// + /// The on which this method is called. + /// A read-only . + /// is . + /// + /// This method may safely be called more than once for the same : + /// it will return the same collection instead of trying to parse the request body again. + /// + public static NameValueCollection GetRequestQueryData(this IHttpContext @this) + { + if (!@this.Items.TryGetValue(QueryDataKey, out var previousResult)) + { + NameValueCollection result; + try + { + result = UrlEncodedDataParser.Parse(@this.Request.Url.Query, false); + } + catch (Exception e) + { + @this.Items[FormDataKey] = e; + throw; + } + + @this.Items[FormDataKey] = result; + return result; + } + + switch (previousResult) + { + case NameValueCollection collection: + return collection; + + case Exception exception: + throw exception.RethrowPreservingStackTrace(); + + case null: + throw SelfCheck.Failure($"Previous result of {nameof(HttpContextExtensions)}.{nameof(GetRequestQueryData)} is null."); + + default: + throw SelfCheck.Failure($"Previous result of {nameof(HttpContextExtensions)}.{nameof(GetRequestQueryData)} is of unexpected type {previousResult.GetType().FullName}"); + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpContextExtensions-ResponseStream.cs b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-ResponseStream.cs new file mode 100644 index 0000000..59dd4be --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-ResponseStream.cs @@ -0,0 +1,68 @@ +using System.IO; +using System.IO.Compression; +using System.Text; +using EmbedIO.Internal; + +namespace EmbedIO +{ + partial class HttpContextExtensions + { + /// + /// Wraps the response output stream and returns a that can be used directly. + /// Optional buffering is applied, so that the response may be sent as one instead of using chunked transfer. + /// Proactive negotiation is performed to select the best compression method supported by the client. + /// + /// The on which this method is called. + /// If set to , sent data is collected + /// in a and sent all at once when the returned + /// is disposed; if set to (the default), chunked transfer will be used. + /// if sending compressed data is preferred over + /// sending non-compressed data; otherwise, . + /// + /// A that can be used to write response data. + /// This stream MUST be disposed when finished writing. + /// + /// + public static Stream OpenResponseStream(this IHttpContext @this, bool buffered = false, bool preferCompression = true) + { + // No need to check whether negotiation is successful; + // the returned callback will throw HttpNotAcceptableException if it was not. + _ = @this.Request.TryNegotiateContentEncoding(preferCompression, out var compressionMethod, out var prepareResponse); + prepareResponse(@this.Response); + var stream = buffered ? new BufferingResponseStream(@this.Response) : @this.Response.OutputStream; + + return compressionMethod switch { + CompressionMethod.Gzip => new GZipStream(stream, CompressionMode.Compress), + CompressionMethod.Deflate => new DeflateStream(stream, CompressionMode.Compress), + _ => stream + }; + } + + /// + /// Wraps the response output stream and returns a that can be used directly. + /// Optional buffering is applied, so that the response may be sent as one instead of using chunked transfer. + /// Proactive negotiation is performed to select the best compression method supported by the client. + /// + /// The on which this method is called. + /// + /// The to use to convert text to data bytes. + /// If (the default), (UTF-8 without a byte order mark) is used. + /// + /// If set to , sent data is collected + /// in a and sent all at once when the returned + /// is disposed; if set to (the default), chunked transfer will be used. + /// if sending compressed data is preferred over + /// sending non-compressed data; otherwise, . + /// + /// A that can be used to write response data. + /// This writer MUST be disposed when finished writing. + /// + /// + public static TextWriter OpenResponseText(this IHttpContext @this, Encoding? encoding = null, bool buffered = false, bool preferCompression = true) + { + encoding ??= WebServer.DefaultEncoding; + @this.Response.ContentEncoding = encoding; + return new StreamWriter(OpenResponseStream(@this, buffered, preferCompression), encoding); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Responses.cs b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Responses.cs new file mode 100644 index 0000000..3447ad4 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpContextExtensions-Responses.cs @@ -0,0 +1,124 @@ +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using EmbedIO.Utilities; + +namespace EmbedIO +{ + partial class HttpContextExtensions + { + private const string StandardHtmlHeaderFormat = "{0} - {1}

{0} - {1}

"; + private const string StandardHtmlFooter = ""; + + /// + /// Asynchronously sends a string as response. + /// + /// The interface on which this method is called. + /// The response content. + /// The MIME type of the content. If , the content type will not be set. + /// The to use. + /// A representing the ongoing operation. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static async Task SendStringAsync( + this IHttpContext @this, + string content, + string contentType, + Encoding encoding) + { + content = Validate.NotNull(nameof(content), content); + encoding = Validate.NotNull(nameof(encoding), encoding); + + if (contentType != null) + { + @this.Response.ContentType = contentType; + @this.Response.ContentEncoding = encoding; + } + + using var text = @this.OpenResponseText(encoding); + await text.WriteAsync(content).ConfigureAwait(false); + } + + /// + /// Asynchronously sends a standard HTML response for the specified status code. + /// + /// The interface on which this method is called. + /// The HTTP status code of the response. + /// A representing the ongoing operation. + /// is . + /// There is no standard status description for . + /// + public static Task SendStandardHtmlAsync(this IHttpContext @this, int statusCode) + => SendStandardHtmlAsync(@this, statusCode, null); + + /// + /// Asynchronously sends a standard HTML response for the specified status code. + /// + /// The interface on which this method is called. + /// The HTTP status code of the response. + /// A callback function that may write additional HTML code + /// to a representing the response output. + /// If not , the callback is called immediately before closing the HTML body tag. + /// A representing the ongoing operation. + /// is . + /// There is no standard status description for . + /// + public static Task SendStandardHtmlAsync( + this IHttpContext @this, + int statusCode, + Action? writeAdditionalHtml) + { + if (!HttpStatusDescription.TryGet(statusCode, out var statusDescription)) + throw new ArgumentException("Status code has no standard description.", nameof(statusCode)); + + @this.Response.StatusCode = statusCode; + @this.Response.StatusDescription = statusDescription; + @this.Response.ContentType = MimeType.Html; + @this.Response.ContentEncoding = WebServer.DefaultEncoding; + using (var text = @this.OpenResponseText(WebServer.DefaultEncoding)) + { + text.Write(StandardHtmlHeaderFormat, statusCode, statusDescription, WebServer.DefaultEncoding.WebName); + writeAdditionalHtml?.Invoke(text); + text.Write(StandardHtmlFooter); + } + + return Task.CompletedTask; + } + + /// + /// Asynchronously sends serialized data as a response, using the default response serializer. + /// As of EmbedIO version 3.0, the default response serializer has the same behavior of JSON + /// response methods of version 2. + /// + /// The interface on which this method is called. + /// The data to serialize. + /// A representing the ongoing operation. + /// is . + /// + /// + public static Task SendDataAsync(this IHttpContext @this, object data) + => ResponseSerializer.Default(@this, data); + + /// + /// Asynchronously sends serialized data as a response, using the specified response serializer. + /// As of EmbedIO version 3.0, the default response serializer has the same behavior of JSON + /// response methods of version 2. + /// + /// The interface on which this method is called. + /// A used to prepare the response. + /// The data to serialize. + /// A representing the ongoing operation. + /// is . + /// is . + /// + /// + public static Task SendDataAsync(this IHttpContext @this, ResponseSerializerCallback serializer, object data) + => Validate.NotNull(nameof(serializer), serializer)(@this, data); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpContextExtensions.cs b/Vendor/EmbedIO-3.5.2/HttpContextExtensions.cs new file mode 100644 index 0000000..46da20c --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpContextExtensions.cs @@ -0,0 +1,30 @@ +using System; +using EmbedIO.Utilities; +using Swan; + +namespace EmbedIO +{ + /// + /// Provides extension methods for types implementing . + /// + public static partial class HttpContextExtensions + { + /// + /// Gets the underlying interface of an . + /// This API mainly supports the EmbedIO infrastructure; it is not intended to be used directly from your code, + /// unless to fulfill very specific needs in the development of plug-ins (modules, etc.) for EmbedIO. + /// + /// The interface on which this method is called. + /// The underlying interface representing + /// the HTTP context implementation. + /// + /// is . + /// + /// + /// does not implement . + /// + public static IHttpContextImpl GetImplementation(this IHttpContext @this) + => Validate.NotNull(nameof(@this), @this) as IHttpContextImpl + ?? throw SelfCheck.Failure($"{@this.GetType().FullName} does not implement {nameof(IHttpContextImpl)}."); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpException-Shortcuts.cs b/Vendor/EmbedIO-3.5.2/HttpException-Shortcuts.cs new file mode 100644 index 0000000..7c8c50b --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpException-Shortcuts.cs @@ -0,0 +1,158 @@ +using System; +using System.Net; + +namespace EmbedIO +{ + partial class HttpException + { + /// + /// Returns a new instance of that, when thrown, + /// will break the request handling control flow and send a 500 Internal Server Error + /// response to the client. + /// + /// A message to include in the response. + /// The data object to include in the response. + /// + /// A newly-created . + /// + public static HttpException InternalServerError(string? message = null, object? data = null) + => new HttpException(HttpStatusCode.InternalServerError, message, data); + + /// + /// Returns a new instance of that, when thrown, + /// will break the request handling control flow and send a 401 Unauthorized + /// response to the client. + /// + /// A message to include in the response. + /// The data object to include in the response. + /// + /// A newly-created . + /// + public static HttpException Unauthorized(string? message = null, object? data = null) + => new HttpException(HttpStatusCode.Unauthorized, message, data); + + /// + /// Returns a new instance of that, when thrown, + /// will break the request handling control flow and send a 403 Forbidden + /// response to the client. + /// + /// A message to include in the response. + /// The data object to include in the response. + /// A newly-created . + public static HttpException Forbidden(string? message = null, object? data = null) + => new HttpException(HttpStatusCode.Forbidden, message, data); + + /// + /// Returns a new instance of that, when thrown, + /// will break the request handling control flow and send a 400 Bad Request + /// response to the client. + /// + /// A message to include in the response. + /// The data object to include in the response. + /// A newly-created . + public static HttpException BadRequest(string? message = null, object? data = null) + => new HttpException(HttpStatusCode.BadRequest, message, data); + + /// + /// Returns a new instance of that, when thrown, + /// will break the request handling control flow and send a 404 Not Found + /// response to the client. + /// + /// A message to include in the response. + /// The data object to include in the response. + /// A newly-created . + public static HttpException NotFound(string? message = null, object? data = null) + => new HttpException(HttpStatusCode.NotFound, message, data); + + /// + /// Returns a new instance of that, when thrown, + /// will break the request handling control flow and send a 405 Method Not Allowed + /// response to the client. + /// + /// A message to include in the response. + /// The data object to include in the response. + /// A newly-created . + public static HttpException MethodNotAllowed(string? message = null, object? data = null) + => new HttpException(HttpStatusCode.MethodNotAllowed, message, data); + + /// + /// Returns a new instance of that, when thrown, + /// will break the request handling control flow and send a 406 Not Acceptable + /// response to the client. + /// + /// A newly-created . + /// + public static HttpNotAcceptableException NotAcceptable() => new HttpNotAcceptableException(); + + /// + /// Returns a new instance of that, when thrown, + /// will break the request handling control flow and send a 406 Not Acceptable + /// response to the client. + /// + /// A value, or a comma-separated list of values, to set the response's Vary header to. + /// A newly-created . + /// + public static HttpNotAcceptableException NotAcceptable(string vary) => new HttpNotAcceptableException(vary); + + /// + /// Returns a new instance of that, when thrown, + /// will break the request handling control flow and send a 416 Range Not Satisfiable + /// response to the client. + /// + /// A newly-created . + /// + public static HttpRangeNotSatisfiableException RangeNotSatisfiable() => new HttpRangeNotSatisfiableException(); + + /// + /// Returns a new instance of that, when thrown, + /// will break the request handling control flow and send a 416 Range Not Satisfiable + /// response to the client. + /// + /// The total length of the requested resource, expressed in bytes, + /// or to omit the Content-Range header in the response. + /// A newly-created . + /// + public static HttpRangeNotSatisfiableException RangeNotSatisfiable(long? contentLength) + => new HttpRangeNotSatisfiableException(contentLength); + + /// + /// Returns a new instance of that, when thrown, + /// will break the request handling control flow and redirect the client + /// to the specified location, using response status code 302. + /// + /// The redirection target. + /// + /// A newly-created . + /// + public static HttpRedirectException Redirect(string location) + => new HttpRedirectException(location); + + /// + /// Returns a new instance of that, when thrown, + /// will break the request handling control flow and redirect the client + /// to the specified location, using the specified response status code. + /// + /// The redirection target. + /// The status code to set on the response, in the range from 300 to 399. + /// + /// A newly-created . + /// + /// is not in the 300-399 range. + public static HttpRedirectException Redirect(string location, int statusCode) + => new HttpRedirectException(location, statusCode); + + /// + /// Returns a new instance of that, when thrown, + /// will break the request handling control flow and redirect the client + /// to the specified location, using the specified response status code. + /// + /// The redirection target. + /// One of the redirection status codes, to be set on the response. + /// + /// A newly-created . + /// + /// is not a redirection status code. + public static HttpRedirectException Redirect(string location, HttpStatusCode statusCode) + => new HttpRedirectException(location, statusCode); + } +} diff --git a/Vendor/EmbedIO-3.5.2/HttpException.cs b/Vendor/EmbedIO-3.5.2/HttpException.cs new file mode 100644 index 0000000..be3e635 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpException.cs @@ -0,0 +1,105 @@ +using System; +using System.Net; + +namespace EmbedIO +{ + /// + /// When thrown, breaks the request handling control flow + /// and sends an error response to the client. + /// +#pragma warning disable CA1032 // Implement standard exception constructors - they have no meaning here. + public partial class HttpException : Exception, IHttpException +#pragma warning restore CA1032 + { + /// + /// Initializes a new instance of the class, + /// with no message to include in the response. + /// + /// The status code to set on the response. + public HttpException(int statusCode) + { + StatusCode = statusCode; + } + + /// + /// Initializes a new instance of the class, + /// with no message to include in the response. + /// + /// The status code to set on the response. + public HttpException(HttpStatusCode statusCode) + : this((int)statusCode) + { + } + + /// + /// Initializes a new instance of the class, + /// with a message to include in the response. + /// + /// The status code to set on the response. + /// A message to include in the response as plain text. + public HttpException(int statusCode, string? message) + : base(message) + { + StatusCode = statusCode; + HttpExceptionMessage = message; + } + + /// + /// Initializes a new instance of the class, + /// with a message to include in the response. + /// + /// The status code to set on the response. + /// A message to include in the response as plain text. + public HttpException(HttpStatusCode statusCode, string? message) + : this((int)statusCode, message) + { + } + + /// + /// Initializes a new instance of the class, + /// with a message and a data object to include in the response. + /// + /// The status code to set on the response. + /// A message to include in the response as plain text. + /// The data object to include in the response. + public HttpException(int statusCode, string? message, object? data) + : this(statusCode, message) + { + DataObject = data; + } + + /// + /// Initializes a new instance of the class, + /// with a message and a data object to include in the response. + /// + /// The status code to set on the response. + /// A message to include in the response as plain text. + /// The data object to include in the response. + public HttpException(HttpStatusCode statusCode, string? message, object? data) + : this((int)statusCode, message, data) + { + } + + /// + public int StatusCode { get; } + + /// + public object? DataObject { get; } + + /// + string? IHttpException.Message => HttpExceptionMessage; + + // This property is necessary because when an exception with a null Message is thrown + // the CLR provides a standard message. We want null to remain null in IHttpException. + private string? HttpExceptionMessage { get; } + + /// + /// + /// This method does nothing; there is no need to call + /// base.PrepareResponse in overrides of this method. + /// + public virtual void PrepareResponse(IHttpContext context) + { + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpExceptionHandler.cs b/Vendor/EmbedIO-3.5.2/HttpExceptionHandler.cs new file mode 100644 index 0000000..b72efa8 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpExceptionHandler.cs @@ -0,0 +1,153 @@ +using System; +using System.Net; +using System.Runtime.ExceptionServices; +using System.Net.Http; +using System.Threading.Tasks; +using System.Web; +using EmbedIO.Utilities; +using Swan.Logging; + +namespace EmbedIO +{ + /// + /// Provides standard handlers for HTTP exceptions at both module and server level. + /// + /// + /// Where applicable, HTTP exception handlers defined in this class + /// use the and + /// properties to customize + /// their behavior. + /// + /// + /// + public static class HttpExceptionHandler + { + /// + /// Gets the default handler used by . + /// This is the same as . + /// + public static HttpExceptionHandlerCallback Default { get; } = HtmlResponse; + + /// + /// Sends an empty response. + /// + /// An interface representing the context of the request. + /// The HTTP exception. + /// A representing the ongoing operation. +#pragma warning disable CA1801 // Unused parameter + public static Task EmptyResponse(IHttpContext context, IHttpException httpException) +#pragma warning restore CA1801 + => Task.CompletedTask; + + /// + /// Sends a HTTP exception's Message property + /// as a plain text response. + /// This handler does not use the DataObject property. + /// + /// An interface representing the context of the request. + /// The HTTP exception. + /// A representing the ongoing operation. + public static Task PlainTextResponse(IHttpContext context, IHttpException httpException) + => context.SendStringAsync(httpException.Message ?? string.Empty, MimeType.PlainText, WebServer.DefaultEncoding); + + /// + /// Sends a response with a HTML payload + /// briefly describing the error, including contact information and/or a stack trace + /// if specified via the + /// and properties, respectively. + /// This handler does not use the DataObject property. + /// + /// An interface representing the context of the request. + /// The HTTP exception. + /// A representing the ongoing operation. + public static Task HtmlResponse(IHttpContext context, IHttpException httpException) + => context.SendStandardHtmlAsync( + httpException.StatusCode, + text => { + text.Write( + "

Exception type: {0}

Message: {1}", + WebUtility.HtmlEncode(httpException.GetType().FullName ?? ""), + WebUtility.HtmlEncode(httpException.Message)); + + text.Write("


If this error is completely unexpected to you, and you think you should not seeing this page, please contact the server administrator"); + + if (!string.IsNullOrEmpty(ExceptionHandler.ContactInformation)) + text.Write(" ({0})", WebUtility.HtmlEncode(ExceptionHandler.ContactInformation)); + + text.Write(", informing them of the time this error occurred and the action(s) you performed that resulted in this error.

"); + + if (ExceptionHandler.IncludeStackTraces) + { + text.Write( + "

Stack trace:


{0}
", + WebUtility.HtmlEncode(httpException.StackTrace)); + } + }); + + /// + /// Gets a that will serialize a HTTP exception's + /// DataObject property and send it as a JSON response. + /// + /// A used to serialize data and send it to the client. + /// A . + /// is . + public static HttpExceptionHandlerCallback DataResponse(ResponseSerializerCallback serializerCallback) + { + Validate.NotNull(nameof(serializerCallback), serializerCallback); + + return (context, httpException) => serializerCallback(context, httpException.DataObject); + } + + /// + /// Gets a that will serialize a HTTP exception's + /// Message and DataObject properties + /// and send them as a JSON response. + /// The response will be a JSON object with a message property and a data property. + /// + /// A used to serialize data and send it to the client. + /// A . + /// is . + public static HttpExceptionHandlerCallback FullDataResponse(ResponseSerializerCallback serializerCallback) + { + Validate.NotNull(nameof(serializerCallback), serializerCallback); + + return (context, httpException) => serializerCallback(context, new + { + message = httpException.Message, + data = httpException.DataObject, + }); + } + + internal static async Task Handle(string logSource, IHttpContext context, Exception exception, HttpExceptionHandlerCallback? handler) + { + if (handler == null || !(exception is IHttpException httpException)) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + return; + } + + exception.Log(logSource, $"[{context.Id}] HTTP exception {httpException.StatusCode}"); + + try + { + context.Response.SetEmptyResponse(httpException.StatusCode); + context.Response.DisableCaching(); + httpException.PrepareResponse(context); + await handler(context, httpException) + .ConfigureAwait(false); + } + catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) + { + throw; + } + catch (HttpListenerException) + { + throw; + } + catch (Exception exception2) + { + exception2.Log(logSource, $"[{context.Id}] Unhandled exception while handling HTTP exception {httpException.StatusCode}"); + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpExceptionHandlerCallback.cs b/Vendor/EmbedIO-3.5.2/HttpExceptionHandlerCallback.cs new file mode 100644 index 0000000..488f6d7 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpExceptionHandlerCallback.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; + +namespace EmbedIO +{ + /// + /// A callback used to build the contents of the response for an . + /// + /// An interface representing the context of the request. + /// An interface. + /// A representing the ongoing operation. + /// + /// When this delegate is called, the response's status code has already been set and the + /// method has already been called. The only thing left to do is preparing the response's content, according + /// to the property. + /// Any exception thrown by a handler (even a HTTP exception) will go unhandled: the web server + /// will not crash, but processing of the request will be aborted, and the response will be flushed as-is. + /// In other words, it is not a good ides to throw HttpException.NotFound() (or similar) + /// from a handler. + /// + public delegate Task HttpExceptionHandlerCallback(IHttpContext context, IHttpException httpException); +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpHeaderNames.cs b/Vendor/EmbedIO-3.5.2/HttpHeaderNames.cs new file mode 100644 index 0000000..a29846f --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpHeaderNames.cs @@ -0,0 +1,449 @@ +namespace EmbedIO +{ + /// + /// Exposes known HTTP header names. + /// + /// + /// The constants in this class have been extracted from a list of known HTTP header names. + /// The presence of a header name in this class is not a guarantee that EmbedIO supports, + /// or even recognizes, it. Refer to the documentation for each module for information about supported + /// headers. + /// + public static class HttpHeaderNames + { + // The .NET Core sources were taken as reference for this list of constants. + // See https://github.com/dotnet/corefx/blob/master/src/Common/src/System/Net/HttpKnownHeaderNames.cs + // However, not all constants come from there, so be careful not to copy-paste indiscriminately. + + /// + /// The Accept HTTP header. + /// + public const string Accept = "Accept"; + + /// + /// The Accept-Charset HTTP header. + /// + public const string AcceptCharset = "Accept-Charset"; + + /// + /// The Accept-Encoding HTTP header. + /// + public const string AcceptEncoding = "Accept-Encoding"; + + /// + /// The Accept-Language HTTP header. + /// + public const string AcceptLanguage = "Accept-Language"; + + /// + /// The Accept-Patch HTTP header. + /// + public const string AcceptPatch = "Accept-Patch"; + + /// + /// The Accept-Ranges HTTP header. + /// + public const string AcceptRanges = "Accept-Ranges"; + + /// + /// The Access-Control-Allow-Credentials HTTP header. + /// + public const string AccessControlAllowCredentials = "Access-Control-Allow-Credentials"; + + /// + /// The Access-Control-Allow-Headers HTTP header. + /// + public const string AccessControlAllowHeaders = "Access-Control-Allow-Headers"; + + /// + /// The Access-Control-Allow-Methods HTTP header. + /// + public const string AccessControlAllowMethods = "Access-Control-Allow-Methods"; + + /// + /// The Access-Control-Allow-Origin HTTP header. + /// + public const string AccessControlAllowOrigin = "Access-Control-Allow-Origin"; + + /// + /// The Access-Control-Expose-Headers HTTP header. + /// + public const string AccessControlExposeHeaders = "Access-Control-Expose-Headers"; + + /// + /// The Access-Control-Max-Age HTTP header. + /// + public const string AccessControlMaxAge = "Access-Control-Max-Age"; + + /// + /// The Access-Control-Request-Headers HTTP header. + /// + public const string AccessControlRequestHeaders = "Access-Control-Request-Headers"; + + /// + /// The Access-Control-Request-Method HTTP header. + /// + public const string AccessControlRequestMethod = "Access-Control-Request-Method"; + + /// + /// The Age HTTP header. + /// + public const string Age = "Age"; + + /// + /// The Allow HTTP header. + /// + public const string Allow = "Allow"; + + /// + /// The Alt-Svc HTTP header. + /// + public const string AltSvc = "Alt-Svc"; + + /// + /// The Authorization HTTP header. + /// + public const string Authorization = "Authorization"; + + /// + /// The Cache-Control HTTP header. + /// + public const string CacheControl = "Cache-Control"; + + /// + /// The Connection HTTP header. + /// + public const string Connection = "Connection"; + + /// + /// The Content-Disposition HTTP header. + /// + public const string ContentDisposition = "Content-Disposition"; + + /// + /// The Content-Encoding HTTP header. + /// + public const string ContentEncoding = "Content-Encoding"; + + /// + /// The Content-Language HTTP header. + /// + public const string ContentLanguage = "Content-Language"; + + /// + /// The Content-Length HTTP header. + /// + public const string ContentLength = "Content-Length"; + + /// + /// The Content-Location HTTP header. + /// + public const string ContentLocation = "Content-Location"; + + /// + /// The Content-MD5 HTTP header. + /// + public const string ContentMD5 = "Content-MD5"; + + /// + /// The Content-Range HTTP header. + /// + public const string ContentRange = "Content-Range"; + + /// + /// The Content-Security-Policy HTTP header. + /// + public const string ContentSecurityPolicy = "Content-Security-Policy"; + + /// + /// The Content-Type HTTP header. + /// + public const string ContentType = "Content-Type"; + + /// + /// The Cookie HTTP header. + /// + public const string Cookie = "Cookie"; + + /// + /// The Cookie2 HTTP header. + /// + public const string Cookie2 = "Cookie2"; + + /// + /// The Date HTTP header. + /// + public const string Date = "Date"; + + /// + /// The ETag HTTP header. + /// + public const string ETag = "ETag"; + + /// + /// The Expect HTTP header. + /// + public const string Expect = "Expect"; + + /// + /// The Expires HTTP header. + /// + public const string Expires = "Expires"; + + /// + /// The From HTTP header. + /// + public const string From = "From"; + + /// + /// The Host HTTP header. + /// + public const string Host = "Host"; + + /// + /// The If-Match HTTP header. + /// + public const string IfMatch = "If-Match"; + + /// + /// The If-Modified-Since HTTP header. + /// + public const string IfModifiedSince = "If-Modified-Since"; + + /// + /// The If-None-Match HTTP header. + /// + public const string IfNoneMatch = "If-None-Match"; + + /// + /// The If-Range HTTP header. + /// + public const string IfRange = "If-Range"; + + /// + /// The If-Unmodified-Since HTTP header. + /// + public const string IfUnmodifiedSince = "If-Unmodified-Since"; + + /// + /// The Keep-Alive HTTP header. + /// + public const string KeepAlive = "Keep-Alive"; + + /// + /// The Last-Modified HTTP header. + /// + public const string LastModified = "Last-Modified"; + + /// + /// The Link HTTP header. + /// + public const string Link = "Link"; + + /// + /// The Location HTTP header. + /// + public const string Location = "Location"; + + /// + /// The Max-Forwards HTTP header. + /// + public const string MaxForwards = "Max-Forwards"; + + /// + /// The Origin HTTP header. + /// + public const string Origin = "Origin"; + + /// + /// The P3P HTTP header. + /// + public const string P3P = "P3P"; + + /// + /// The Pragma HTTP header. + /// + public const string Pragma = "Pragma"; + + /// + /// The Proxy-Authenticate HTTP header. + /// + public const string ProxyAuthenticate = "Proxy-Authenticate"; + + /// + /// The Proxy-Authorization HTTP header. + /// + public const string ProxyAuthorization = "Proxy-Authorization"; + + /// + /// The Proxy-Connection HTTP header. + /// + public const string ProxyConnection = "Proxy-Connection"; + + /// + /// The Public-Key-Pins HTTP header. + /// + public const string PublicKeyPins = "Public-Key-Pins"; + + /// + /// The Range HTTP header. + /// + public const string Range = "Range"; + + /// + /// The Referer HTTP header. + /// + /// + /// The incorrect spelling ("Referer" instead of "Referrer") is intentional + /// and has historical reasons. + /// See the "Etymology" section of the Wikipedia article + /// on this header for more information. + /// + public const string Referer = "Referer"; + + /// + /// The Retry-After HTTP header. + /// + public const string RetryAfter = "Retry-After"; + + /// + /// The Sec-WebSocket-Accept HTTP header. + /// + public const string SecWebSocketAccept = "Sec-WebSocket-Accept"; + + /// + /// The Sec-WebSocket-Extensions HTTP header. + /// + public const string SecWebSocketExtensions = "Sec-WebSocket-Extensions"; + + /// + /// The Sec-WebSocket-Key HTTP header. + /// + public const string SecWebSocketKey = "Sec-WebSocket-Key"; + + /// + /// The Sec-WebSocket-Protocol HTTP header. + /// + public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol"; + + /// + /// The Sec-WebSocket-Version HTTP header. + /// + public const string SecWebSocketVersion = "Sec-WebSocket-Version"; + + /// + /// The Server HTTP header. + /// + public const string Server = "Server"; + + /// + /// The Set-Cookie HTTP header. + /// + public const string SetCookie = "Set-Cookie"; + + /// + /// The Set-Cookie2 HTTP header. + /// + public const string SetCookie2 = "Set-Cookie2"; + + /// + /// The Strict-Transport-Security HTTP header. + /// + public const string StrictTransportSecurity = "Strict-Transport-Security"; + + /// + /// The TE HTTP header. + /// + public const string TE = "TE"; + + /// + /// The TSV HTTP header. + /// + public const string TSV = "TSV"; + + /// + /// The Trailer HTTP header. + /// + public const string Trailer = "Trailer"; + + /// + /// The Transfer-Encoding HTTP header. + /// + public const string TransferEncoding = "Transfer-Encoding"; + + /// + /// The Upgrade HTTP header. + /// + public const string Upgrade = "Upgrade"; + + /// + /// The Upgrade-Insecure-Requests HTTP header. + /// + public const string UpgradeInsecureRequests = "Upgrade-Insecure-Requests"; + + /// + /// The User-Agent HTTP header. + /// + public const string UserAgent = "User-Agent"; + + /// + /// The Vary HTTP header. + /// + public const string Vary = "Vary"; + + /// + /// The Via HTTP header. + /// + public const string Via = "Via"; + + /// + /// The WWW-Authenticate HTTP header. + /// + public const string WWWAuthenticate = "WWW-Authenticate"; + + /// + /// The Warning HTTP header. + /// + public const string Warning = "Warning"; + + /// + /// The X-AspNet-Version HTTP header. + /// + public const string XAspNetVersion = "X-AspNet-Version"; + + /// + /// The X-Content-Duration HTTP header. + /// + public const string XContentDuration = "X-Content-Duration"; + + /// + /// The X-Content-Type-Options HTTP header. + /// + public const string XContentTypeOptions = "X-Content-Type-Options"; + + /// + /// The X-Frame-Options HTTP header. + /// + public const string XFrameOptions = "X-Frame-Options"; + + /// + /// The X-MSEdge-Ref HTTP header. + /// + public const string XMSEdgeRef = "X-MSEdge-Ref"; + + /// + /// The X-Powered-By HTTP header. + /// + public const string XPoweredBy = "X-Powered-By"; + + /// + /// The X-Request-ID HTTP header. + /// + public const string XRequestID = "X-Request-ID"; + + /// + /// The X-UA-Compatible HTTP header. + /// + public const string XUACompatible = "X-UA-Compatible"; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpListenerMode.cs b/Vendor/EmbedIO-3.5.2/HttpListenerMode.cs new file mode 100644 index 0000000..3e10107 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpListenerMode.cs @@ -0,0 +1,20 @@ +namespace EmbedIO +{ + /// + /// Defines the HTTP listeners available for use in a . + /// + public enum HttpListenerMode + { + /// + /// Use EmbedIO's internal HTTP listener implementation, + /// based on Mono's System.Net.HttpListener. + /// + EmbedIO, + + /// + /// Use the class + /// provided by the .NET runtime in use. + /// + Microsoft, + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpNotAcceptableException.cs b/Vendor/EmbedIO-3.5.2/HttpNotAcceptableException.cs new file mode 100644 index 0000000..f84f47e --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpNotAcceptableException.cs @@ -0,0 +1,55 @@ +using System.Net; + +namespace EmbedIO +{ + /// + /// When thrown, breaks the request handling control flow + /// and sends a redirection response to the client. + /// +#pragma warning disable CA1032 // Implement standard exception constructors - they have no meaning here. + public class HttpNotAcceptableException : HttpException +#pragma warning restore CA1032 + { + /// + /// Initializes a new instance of the class, + /// without specifying a value for the response's Vary header. + /// + public HttpNotAcceptableException() + : this(null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// A value, or a comma-separated list of values, to set the response's Vary header to. + /// Although not specified in RFC7231, + /// this may help the client to understand why the request has been rejected. + /// If this parameter is or the empty string, the response's Vary header + /// is not set. + /// + public HttpNotAcceptableException(string? vary) + : base((int)HttpStatusCode.NotAcceptable) + { + Vary = string.IsNullOrEmpty(vary) ? null : vary; + } + + /// + /// Gets the value, or comma-separated list of values, to be set + /// on the response's Vary header. + /// + /// + /// If the empty string has been passed to the + /// constructor, the value of this property is . + /// + public string? Vary { get; } + + /// + public override void PrepareResponse(IHttpContext context) + { + if (Vary != null) + context.Response.Headers.Add(HttpHeaderNames.Vary, Vary); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpRangeNotSatisfiableException.cs b/Vendor/EmbedIO-3.5.2/HttpRangeNotSatisfiableException.cs new file mode 100644 index 0000000..8397e1b --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpRangeNotSatisfiableException.cs @@ -0,0 +1,50 @@ +using System.Net; + +namespace EmbedIO +{ + /// + /// When thrown, breaks the request handling control flow + /// and sends a redirection response to the client. + /// +#pragma warning disable CA1032 // Implement standard exception constructors - they have no meaning here. + public class HttpRangeNotSatisfiableException : HttpException +#pragma warning restore CA1032 + { + /// + /// Initializes a new instance of the class. + /// without specifying a value for the response's Content-Range header. + /// + public HttpRangeNotSatisfiableException() + : this(null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The total length of the requested resource, expressed in bytes, + /// or to omit the Content-Range header in the response. + public HttpRangeNotSatisfiableException(long? contentLength) + : base((int)HttpStatusCode.RequestedRangeNotSatisfiable) + { + ContentLength = contentLength; + } + + /// + /// Gets the total content length to be specified + /// on the response's Content-Range header. + /// + public long? ContentLength { get; } + + /// + public override void PrepareResponse(IHttpContext context) + { + // RFC 7233, Section 3.1: "When this status code is generated in response + // to a byte-range request, the sender + // SHOULD generate a Content-Range header field specifying + // the current length of the selected representation." + if (ContentLength.HasValue) + context.Response.Headers.Set(HttpHeaderNames.ContentRange, $"bytes */{ContentLength.Value}"); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpRedirectException.cs b/Vendor/EmbedIO-3.5.2/HttpRedirectException.cs new file mode 100644 index 0000000..22b6801 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpRedirectException.cs @@ -0,0 +1,54 @@ +using System; +using System.Net; + +namespace EmbedIO +{ + /// + /// When thrown, breaks the request handling control flow + /// and sends a redirection response to the client. + /// +#pragma warning disable CA1032 // Implement standard exception constructors - they have no meaning here. + public class HttpRedirectException : HttpException +#pragma warning restore CA1032 + { + /// + /// Initializes a new instance of the class. + /// + /// The redirection target. + /// + /// The status code to set on the response, in the range from 300 to 399. + /// By default, status code 302 (Found) is used. + /// + /// is not in the 300-399 range. + public HttpRedirectException(string location, int statusCode = (int)HttpStatusCode.Found) + : base(statusCode) + { + if (statusCode < 300 || statusCode > 399) + throw new ArgumentException("Redirect status code is not valid.", nameof(statusCode)); + + Location = location; + } + + /// + /// Initializes a new instance of the class. + /// + /// The redirection target. + /// One of the redirection status codes, to be set on the response. + /// is not a redirection status code. + public HttpRedirectException(string location, HttpStatusCode statusCode) + : this(location, (int)statusCode) + { + } + + /// + /// Gets the URL where the client will be redirected. + /// + public string Location { get; } + + /// + public override void PrepareResponse(IHttpContext context) + { + context.Redirect(Location, StatusCode); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpRequestExtensions.cs b/Vendor/EmbedIO-3.5.2/HttpRequestExtensions.cs new file mode 100644 index 0000000..70e55aa --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpRequestExtensions.cs @@ -0,0 +1,262 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http.Headers; +using EmbedIO.Utilities; + +namespace EmbedIO +{ + /// + /// Provides extension methods for types implementing . + /// + public static class HttpRequestExtensions + { + /// + /// Returns a string representing the remote IP address and port of an interface. + /// This method can be called even on a interface, or one that has no + /// remote end point, or no remote address; it will always return a non-, + /// non-empty string. + /// + /// The on which this method is called. + /// + /// If is , or its RemoteEndPoint + /// is , the string "<null>; otherwise, the remote end point's + /// Address (or the string "<???>" if it is ) + /// followed by a colon and the Port number. + /// + public static string SafeGetRemoteEndpointStr(this IHttpRequest @this) + { + var endPoint = @this?.RemoteEndPoint; + return endPoint == null + ? "" + : $"{endPoint.Address?.ToString() ?? ""}:{endPoint.Port.ToString(CultureInfo.InvariantCulture)}"; + } + + /// + /// Attempts to proactively negotiate a compression method for a response, + /// based on a request's Accept-Encoding header (or lack of it). + /// + /// The on which this method is called. + /// if sending compressed data is preferred over + /// sending non-compressed data; otherwise, . + /// When this method returns, the compression method to use for the response, + /// if content negotiation is successful. This parameter is passed uninitialized. + /// When this method returns, a callback that prepares data in an + /// according to the result of content negotiation. This parameter is passed uninitialized. + /// if content negotiation is successful; + /// otherwise, . + /// + /// If this method returns , the callback + /// will set appropriate response headers to reflect the results of content negotiation. + /// If this method returns , the callback + /// will throw a to send a 406 Not Acceptable response + /// with the Vary header set to Accept-Encoding, + /// so that the client may know the reason why the request has been rejected. + /// If has noAccept-Encoding header, this method + /// always returns and sets + /// to . + /// + /// + public static bool TryNegotiateContentEncoding( + this IHttpRequest @this, + bool preferCompression, + out CompressionMethod compressionMethod, + out Action prepareResponse) + { + var acceptedEncodings = new QValueList(true, @this.Headers.GetValues(HttpHeaderNames.AcceptEncoding)); + if (!acceptedEncodings.TryNegotiateContentEncoding(preferCompression, out compressionMethod, out var compressionMethodName)) + { + prepareResponse = r => throw HttpException.NotAcceptable(HttpHeaderNames.AcceptEncoding); + return false; + } + + prepareResponse = r => { + r.Headers.Add(HttpHeaderNames.Vary, HttpHeaderNames.AcceptEncoding); + r.Headers.Set(HttpHeaderNames.ContentEncoding, compressionMethodName); + }; + return true; + } + + /// + /// Checks whether an If-None-Match header exists in a request + /// and, if so, whether it contains a given entity tag. + /// See RFC7232, Section 3.2 + /// for a normative reference; however, see the Remarks section for more information + /// about the RFC compliance of this method. + /// + /// The on which this method is called. + /// The entity tag. + /// When this method returns, a value that indicates whether an + /// If-None-Match header is present in , regardless of the method's + /// return value. This parameter is passed uninitialized. + /// if an If-None-Match header is present in + /// and one of the entity tags listed in it is equal to ; + /// otherwise. + /// + /// RFC7232, Section 3.2 + /// states that a weak comparison function (as defined in + /// RFC7232, Section 2.3.2) + /// must be used for If-None-Match. That would mean parsing every entity tag, at least minimally, + /// to determine whether it is a "weak" or "strong" tag. Since EmbedIO currently generates only + /// "strong" tags, this method uses the default string comparer instead. + /// The behavior of this method is thus not, strictly speaking, RFC7232-compliant; + /// it works, though, with entity tags generated by EmbedIO. + /// + public static bool CheckIfNoneMatch(this IHttpRequest @this, string entityTag, out bool headerExists) + { + var values = @this.Headers.GetValues(HttpHeaderNames.IfNoneMatch); + if (values == null) + { + headerExists = false; + return false; + } + + headerExists = true; + return values.Select(t => t.Trim()).Contains(entityTag); + } + + // Check whether the If-Modified-Since request header exists + // and specifies a date and time more recent than or equal to + // the date and time of last modification of the requested resource. + // RFC7232, Section 3.3 + + /// + /// Checks whether an If-Modified-Since header exists in a request + /// and, if so, whether its value is a date and time more recent or equal to + /// a given . + /// See RFC7232, Section 3.3 + /// for a normative reference. + /// + /// The on which this method is called. + /// A date and time value, in Coordinated Universal Time, + /// expressing the last time a resource was modified. + /// When this method returns, a value that indicates whether an + /// If-Modified-Since header is present in , regardless of the method's + /// return value. This parameter is passed uninitialized. + /// if an If-Modified-Since header is present in + /// and its value is a date and time more recent or equal to ; + /// otherwise. + public static bool CheckIfModifiedSince(this IHttpRequest @this, DateTime lastModifiedUtc, out bool headerExists) + { + var value = @this.Headers.Get(HttpHeaderNames.IfModifiedSince); + if (value == null) + { + headerExists = false; + return false; + } + + headerExists = true; + return HttpDate.TryParse(value, out var dateTime) + && dateTime.UtcDateTime >= lastModifiedUtc; + } + + // Checks the Range request header to tell whether to send + // a "206 Partial Content" response. + + /// + /// Checks whether a Range header exists in a request + /// and, if so, determines whether it is possible to send a 206 Partial Content response. + /// See RFC7233 + /// for a normative reference; however, see the Remarks section for more information + /// about the RFC compliance of this method. + /// + /// The on which this method is called. + /// The total length, in bytes, of the response entity, i.e. + /// what would be sent in a 200 OK response. + /// An entity tag representing the response entity. This value is checked against + /// the If-Range header, if it is present. + /// The date and time value, in Coordinated Universal Time, + /// expressing the last modification time of the resource entity. This value is checked against + /// the If-Range header, if it is present. + /// When this method returns , the start of the requested byte range. + /// This parameter is passed uninitialized. + /// + /// When this method returns , the upper bound of the requested byte range. + /// This parameter is passed uninitialized. + /// Note that the upper bound of a range is NOT the sum of the range's start and length; + /// for example, a range expressed as bytes=0-99 has a start of 0, an upper bound of 99, + /// and a length of 100 bytes. + /// + /// + /// This method returns if the following conditions are satisfied: + /// + /// >the request's HTTP method is GET; + /// >a Range header is present in the request; + /// >either no If-Range header is present in the request, or it + /// specifies an entity tag equal to , or a UTC date and time + /// equal to ; + /// >the Range header specifies exactly one range; + /// >the specified range is entirely contained in the range from 0 to - 1. + /// + /// If the last condition is not satisfied, i.e. the specified range start and/or upper bound + /// are out of the range from 0 to - 1, this method does not return; + /// it throws a instead. + /// If any of the other conditions are not satisfied, this method returns . + /// + /// + /// According to RFC7233, Section 3.1, + /// there are several conditions under which a server may ignore or reject a range request; therefore, + /// clients are (or should be) prepared to receive a 200 OK response with the whole response + /// entity instead of the requested range(s). For this reason, until the generation of + /// multipart/byteranges responses is implemented in EmbedIO, this method will ignore + /// range requests specifying more than one range, even if this behavior is not, strictly speaking, + /// RFC7233-compliant. + /// To make clients aware that range requests are accepted for a resource, every 200 OK + /// (or 304 Not Modified) response for the same resource should include an Accept-Ranges + /// header with the string bytes as value. + /// + public static bool IsRangeRequest(this IHttpRequest @this, long contentLength, string entityTag, DateTime lastModifiedUtc, out long start, out long upperBound) + { + start = 0; + upperBound = contentLength - 1; + + // RFC7233, Section 3.1: + // "A server MUST ignore a Range header field received with a request method other than GET." + if (@this.HttpVerb != HttpVerbs.Get) + return false; + + // No Range header, no partial content. + var rangeHeader = @this.Headers.Get(HttpHeaderNames.Range); + if (rangeHeader == null) + return false; + + // Ignore the Range header if there is no If-Range header + // or if the If-Range header specifies a non-matching validator. + // RFC7233, Section 3.2: "If the validator given in the If-Range header field matches the + // current validator for the selected representation of the target + // resource, then the server SHOULD process the Range header field as + // requested.If the validator does not match, the server MUST ignore + // the Range header field.Note that this comparison by exact match, + // including when the validator is an HTTP-date, differs from the + // "earlier than or equal to" comparison used when evaluating an + // If-Unmodified-Since conditional." + var ifRange = @this.Headers.Get(HttpHeaderNames.IfRange)?.Trim(); + if (ifRange != null && ifRange != entityTag) + { + if (!HttpDate.TryParse(ifRange, out var rangeDate)) + return false; + + if (rangeDate.UtcDateTime != lastModifiedUtc) + return false; + } + + // Ignore the Range request header if it cannot be parsed successfully. + if (!RangeHeaderValue.TryParse(rangeHeader, out var range)) + return false; + + // EmbedIO does not support multipart/byteranges responses (yet), + // thus ignore range requests that specify one range. + if (range.Ranges.Count != 1) + return false; + + var firstRange = range.Ranges.First(); + start = firstRange.From ?? 0L; + upperBound = firstRange.To ?? contentLength - 1; + if (start >= contentLength || upperBound < start || upperBound >= contentLength) + throw HttpException.RangeNotSatisfiable(contentLength); + + return true; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpResponseExtensions.cs b/Vendor/EmbedIO-3.5.2/HttpResponseExtensions.cs new file mode 100644 index 0000000..ad4c6c4 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpResponseExtensions.cs @@ -0,0 +1,43 @@ +using System; +using EmbedIO.Utilities; + +namespace EmbedIO +{ + /// + /// Provides extension methods for types implementing . + /// + public static class HttpResponseExtensions + { + /// + /// Sets the necessary headers to disable caching of a response on the client side. + /// + /// The interface on which this method is called. + /// is . + public static void DisableCaching(this IHttpResponse @this) + { + var headers = @this.Headers; + headers.Set(HttpHeaderNames.Expires, "Sat, 26 Jul 1997 05:00:00 GMT"); + headers.Set(HttpHeaderNames.LastModified, HttpDate.Format(DateTime.UtcNow)); + headers.Set(HttpHeaderNames.CacheControl, "no-store, no-cache, must-revalidate"); + headers.Add(HttpHeaderNames.Pragma, "no-cache"); + } + + /// + /// Prepares a standard response without a body for the specified status code. + /// + /// The interface on which this method is called. + /// The HTTP status code of the response. + /// is . + /// There is no standard status description for . + public static void SetEmptyResponse(this IHttpResponse @this, int statusCode) + { + if (!HttpStatusDescription.TryGet(statusCode, out var statusDescription)) + throw new ArgumentException("Status code has no standard description.", nameof(statusCode)); + + @this.StatusCode = statusCode; + @this.StatusDescription = statusDescription; + @this.ContentType = MimeType.Default; + @this.ContentEncoding = null; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpStatusDescription.cs b/Vendor/EmbedIO-3.5.2/HttpStatusDescription.cs new file mode 100644 index 0000000..a724005 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpStatusDescription.cs @@ -0,0 +1,146 @@ +using System.Collections.Generic; +using System.Net; + +namespace EmbedIO +{ + /// + /// Provides standard HTTP status descriptions. + /// Data contained in this class comes from the following sources: + /// + /// RFC7231 Section 6 (HTTP/1.1 Semantics and Content) + /// RFC6585 (Additional HTTP Status Codes) + /// RFC2774 Section 7 (An HTTP Extension Framework) + /// RFC7540 Section 9.1.2 (HTTP/2) + /// RFC4918 Section 11 (WebDAV) + /// RFC5842 Section 7 (Binding Extensions to WebDAV) + /// RFC7538 Section 3 (HTTP Status Code 308) + /// RFC3229 Section 10.4.1 (Delta encoding in HTTP) + /// RFC8297 Section 2 (Early Hints) + /// RFC7725 Section 3 (HTTP-status-451) + /// RFC2295 Section 8.1 (Transparent Content Negotiation) + /// + /// + public static class HttpStatusDescription + { + private static readonly IReadOnlyDictionary Dictionary = new Dictionary { + { 100, "Continue" }, + { 101, "Switching Protocols" }, + { 102, "Processing" }, + { 103, "Early Hints" }, + { 200, "OK" }, + { 201, "Created" }, + { 202, "Accepted" }, + { 203, "Non-Authoritative Information" }, + { 204, "No Content" }, + { 205, "Reset Content" }, + { 206, "Partial Content" }, + { 207, "Multi-Status" }, + { 208, "Already Reported" }, + { 226, "IM Used" }, + { 300, "Multiple Choices" }, + { 301, "Moved Permanently" }, + { 302, "Found" }, + { 303, "See Other" }, + { 304, "Not Modified" }, + { 305, "Use Proxy" }, + { 307, "Temporary Redirect" }, + { 308, "Permanent Redirect" }, + { 400, "Bad Request" }, + { 401, "Unauthorized" }, + { 402, "Payment Required" }, + { 403, "Forbidden" }, + { 404, "Not Found" }, + { 405, "Method Not Allowed" }, + { 406, "Not Acceptable" }, + { 407, "Proxy Authentication Required" }, + { 408, "Request Timeout" }, + { 409, "Conflict" }, + { 410, "Gone" }, + { 411, "Length Required" }, + { 412, "Precondition Failed" }, + { 413, "Request Entity Too Large" }, + { 414, "Request-Uri Too Long" }, + { 415, "Unsupported Media Type" }, + { 416, "Requested Range Not Satisfiable" }, + { 417, "Expectation Failed" }, + { 421, "Misdirected Request" }, + { 422, "Unprocessable Entity" }, + { 423, "Locked" }, + { 424, "Failed Dependency" }, + { 426, "Upgrade Required" }, + { 428, "Precondition Required" }, + { 429, "Too Many Requests" }, + { 431, "Request Header Fields Too Large" }, + { 451, "Unavailable For Legal Reasons" }, + { 500, "Internal Server Error" }, + { 501, "Not Implemented" }, + { 502, "Bad Gateway" }, + { 503, "Service Unavailable" }, + { 504, "Gateway Timeout" }, + { 505, "Http Version Not Supported" }, + { 506, "Variant Also Negotiates" }, + { 507, "Insufficient Storage" }, + { 508, "Loop Detected" }, + { 510, "Not Extended" }, + { 511, "Network Authentication Required" }, + }; + + /// + /// Attempts to get the standard status description for a . + /// + /// The HTTP status code for which the standard description + /// is to be retrieved. + /// When this method returns, the standard HTTP status description + /// for the specified if it was found, or + /// if it was not found. This parameter is passed uninitialized. + /// if the specified was found + /// in the list of HTTP status codes for which the standard description is known; + /// otherwise, . + /// + /// + public static bool TryGet(HttpStatusCode code, out string description) => Dictionary.TryGetValue((int)code, out description); + + /// + /// Attempts to get the standard status description for a HTTP status code + /// specified as an . + /// + /// The HTTP status code for which the standard description + /// is to be retrieved. + /// When this method returns, the standard HTTP status description + /// for the specified if it was found, or + /// if it was not found. This parameter is passed uninitialized. + /// if the specified was found + /// in the list of HTTP status codes for which the standard description is known; + /// otherwise, . + /// + /// + public static bool TryGet(int code, out string description) => Dictionary.TryGetValue(code, out description); + + /// + /// Returns the standard status description for a . + /// + /// The HTTP status code for which the standard description + /// is to be retrieved. + /// The standard HTTP status description for the specified + /// if it was found, or if it was not found. + public static string Get(HttpStatusCode code) + { + Dictionary.TryGetValue((int)code, out var description); + return description; + } + + /// + /// Returns the standard status description for a HTTP status code + /// specified as an . + /// + /// The HTTP status code for which the standard description + /// is to be retrieved. + /// The standard HTTP status description for the specified + /// if it was found, or if it was not found. + public static string Get(int code) + { + Dictionary.TryGetValue(code, out var description); + return description; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/HttpVerbs.cs b/Vendor/EmbedIO-3.5.2/HttpVerbs.cs new file mode 100644 index 0000000..765b01e --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/HttpVerbs.cs @@ -0,0 +1,48 @@ +namespace EmbedIO +{ + /// + /// Enumerates the different HTTP Verbs. + /// + public enum HttpVerbs + { + /// + /// Wildcard Method + /// + Any, + + /// + /// DELETE Method + /// + Delete, + + /// + /// GET Method + /// + Get, + + /// + /// HEAD method + /// + Head, + + /// + /// OPTIONS method + /// + Options, + + /// + /// PATCH method + /// + Patch, + + /// + /// POST method + /// + Post, + + /// + /// PUT method + /// + Put, + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/ICookieCollection.cs b/Vendor/EmbedIO-3.5.2/ICookieCollection.cs new file mode 100644 index 0000000..48f4f8f --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/ICookieCollection.cs @@ -0,0 +1,49 @@ +using System.Net; +using System.Collections; +using System.Collections.Generic; + +namespace EmbedIO +{ + /// + /// Interface for Cookie Collection. + /// + /// +#pragma warning disable CA1010 // Should implement ICollection - not possible when wrapping System.Net.CookieCollection. + public interface ICookieCollection : IEnumerable, ICollection +#pragma warning restore CA1010 + { + /// + /// Gets the with the specified name. + /// + /// + /// The . + /// + /// The name. + /// The cookie matching the specified name. + Cookie? this[string name] { get; } + + /// + /// Determines whether this contains the specified . + /// + /// The cookie to find in the . + /// + /// if this contains the specified ; + /// otherwise, . + /// + bool Contains(Cookie cookie); + + /// + /// Copies the elements of this to a array + /// starting at the specified index of the target array. + /// + /// The target array to which the will be copied. + /// The zero-based index in the target where copying begins. + void CopyTo(Cookie[] array, int index); + + /// + /// Adds the specified cookie. + /// + /// The cookie. + void Add(Cookie cookie); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/IHttpContext.cs b/Vendor/EmbedIO-3.5.2/IHttpContext.cs new file mode 100644 index 0000000..bed7695 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/IHttpContext.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Routing; +using EmbedIO.Sessions; + +namespace EmbedIO +{ + /// + /// Represents the context of a HTTP(s) request being handled by a web server. + /// + public interface IHttpContext : IMimeTypeProvider + { + /// + /// Gets a unique identifier for a HTTP context. + /// + string Id { get; } + + /// + /// Gets a used to stop processing of this context. + /// + CancellationToken CancellationToken { get; } + + /// + /// Gets the server IP address and port number to which the request is directed. + /// + IPEndPoint LocalEndPoint { get; } + + /// + /// Gets the client IP address and port number from which the request originated. + /// + IPEndPoint RemoteEndPoint { get; } + + /// + /// Gets the HTTP request. + /// + IHttpRequest Request { get; } + + /// + /// Gets the route matched by the requested URL path. + /// + RouteMatch Route { get; } + + /// + /// Gets the requested path, relative to the innermost module's base path. + /// + /// + /// This property derives from the path specified in the requested URL, stripped of the + /// BaseRoute of the handling module. + /// This property is in itself a valid URL path, including an initial + /// slash (/) character. + /// + string RequestedPath { get; } + + /// + /// Gets the HTTP response object. + /// + IHttpResponse Response { get; } + + /// + /// Gets the user. + /// + IPrincipal User { get; } + + /// + /// Gets the session proxy associated with this context. + /// + ISessionProxy Session { get; } + + /// + /// Gets a value indicating whether compressed request bodies are supported. + /// + /// + bool SupportCompressedRequests { get; } + + /// + /// Gets the dictionary of data to pass trough the EmbedIO pipeline. + /// + IDictionary Items { get; } + + /// + /// Gets the elapsed time, expressed in milliseconds, since the creation of this context. + /// + long Age { get; } + + /// + /// Gets a value indicating whether this + /// has been completely handled, so that no further processing is required. + /// When a HTTP context is created, this property is ; + /// as soon as it is set to , the context is not + /// passed to any further module's handler for processing. + /// Once it becomes , this property is guaranteed + /// to never become again. + /// + /// + /// When a module's IsFinalHandler property is + /// , this property is set to after the + /// returned by the module's HandleRequestAsync method + /// is completed. + /// + /// + /// + bool IsHandled { get; } + + /// + /// Marks this context as handled, so that it will not be + /// processed by any further module. + /// + /// + /// Calling this method from the + /// or of a module whose + /// property is + /// is redundant and has no effect. + /// + /// + /// + void SetHandled(); + + /// + /// Registers a callback to be called when processing is finished on a context. + /// + /// The callback. + void OnClose(Action callback); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/IHttpContextHandler.cs b/Vendor/EmbedIO-3.5.2/IHttpContextHandler.cs new file mode 100644 index 0000000..ed0f311 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/IHttpContextHandler.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace EmbedIO +{ + /// + /// Represents an object that can handle a HTTP context. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + public interface IHttpContextHandler + { + /// + /// Asynchronously handles a HTTP context, generating a suitable response + /// for an incoming request. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + /// The HTTP context. + /// A representing the ongoing operation. + Task HandleContextAsync(IHttpContextImpl context); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/IHttpContextImpl.cs b/Vendor/EmbedIO-3.5.2/IHttpContextImpl.cs new file mode 100644 index 0000000..de061bb --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/IHttpContextImpl.cs @@ -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 +{ + /// + /// Represents a HTTP context implementation, i.e. a HTTP context as seen internally by EmbedIO. + /// 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). + /// + /// + public interface IHttpContextImpl : IHttpContext + { + /// + /// Gets or sets a used to stop processing of this context. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + new CancellationToken CancellationToken { get; set; } + + /// + /// Gets or sets the route matched by the requested URL path. + /// + new RouteMatch Route { get; set; } + + /// + /// Gets or sets the session proxy associated with this context. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + /// + /// An interface. + /// + new ISessionProxy Session { get; set; } + + /// + /// Gets or sets the user. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + new IPrincipal User { get; set; } + + /// + /// Gets or sets a value indicating whether compressed request bodies are supported. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + /// + new bool SupportCompressedRequests { get; set; } + + /// + /// Gets the MIME type providers. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + MimeTypeProviderStack MimeTypeProviders { get; } + + /// + /// Flushes and closes the response stream, then calls any registered close callbacks. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + /// + void Close(); + + /// + /// Asynchronously handles a WebSockets opening handshake + /// and returns a newly-created interface. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + /// The requested WebSocket sub-protocols. + /// The accepted WebSocket sub-protocol, + /// or the empty string is no sub-protocol has been agreed upon. + /// Size of the receive buffer. + /// The keep-alive interval. + /// A used to stop the server. + /// + /// An interface. + /// + Task AcceptWebSocketAsync( + IEnumerable requestedProtocols, + string acceptedProtocol, + int receiveBufferSize, + TimeSpan keepAliveInterval, + CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/IHttpException.cs b/Vendor/EmbedIO-3.5.2/IHttpException.cs new file mode 100644 index 0000000..0349a16 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/IHttpException.cs @@ -0,0 +1,58 @@ +using System; + +namespace EmbedIO +{ + /// + /// Represents an exception that results in a particular + /// HTTP response to be sent to the client. + /// This interface is meant to be implemented + /// by classes derived from . + /// 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. + /// + /// + /// + public interface IHttpException + { + /// + /// Gets the response status code for a HTTP exception. + /// + int StatusCode { get; } + + /// + /// Gets the stack trace of a HTTP exception. + /// + string StackTrace { get; } + + /// + /// Gets a message that can be included in the response triggered + /// by a HTTP exception. + /// Whether the message is actually sent to the client will depend + /// upon the handler used to send the response. + /// + /// + /// Do not rely on to implement + /// this property if you want to support messages, + /// because a default message will be supplied by the CLR at throw time + /// when is . + /// + string? Message { get; } + + /// + /// Gets an object that can be serialized and included + /// in the response triggered by a HTTP exception. + /// Whether the object is actually sent to the client will depend + /// upon the handler used to send the response. + /// + object? DataObject { get; } + + /// + /// Sets necessary headers, as required by the nature + /// of the HTTP exception (e.g. Location for + /// ). + /// + /// The HTTP context of the response. + void PrepareResponse(IHttpContext context); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/IHttpListener.cs b/Vendor/EmbedIO-3.5.2/IHttpListener.cs new file mode 100644 index 0000000..3ec376c --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/IHttpListener.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace EmbedIO +{ + /// + /// Interface to create a HTTP Listener. + /// + public interface IHttpListener : IDisposable + { + /// + /// Gets or sets a value indicating whether the listener should ignore write exceptions. By default the flag is set on. + /// + /// + /// true if [ignore write exceptions]; otherwise, false. + /// + bool IgnoreWriteExceptions { get; set; } + + /// + /// Gets the prefixes. + /// + /// + /// The prefixes. + /// + List Prefixes { get; } + + /// + /// Gets a value indicating whether this instance is listening. + /// + /// + /// true if this instance is listening; otherwise, false. + /// + bool IsListening { get; } + + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// + string Name { get; } + + /// + /// Starts this listener. + /// + void Start(); + + /// + /// Stops this listener. + /// +#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 + + /// + /// Adds the prefix. + /// + /// The URL prefix. + void AddPrefix(string urlPrefix); + + /// + /// Gets the HTTP context asynchronous. + /// + /// The cancellation token. + /// + /// A task that represents the time delay for the HTTP Context. + /// + Task GetContextAsync(CancellationToken cancellationToken); + } +} diff --git a/Vendor/EmbedIO-3.5.2/IHttpMessage.cs b/Vendor/EmbedIO-3.5.2/IHttpMessage.cs new file mode 100644 index 0000000..61a5dde --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/IHttpMessage.cs @@ -0,0 +1,26 @@ +using System; + +namespace EmbedIO +{ + /// + /// Represents a HTTP request or response. + /// + public interface IHttpMessage + { + /// + /// Gets the cookies. + /// + /// + /// The cookies. + /// + ICookieCollection Cookies { get; } + + /// + /// Gets or sets the protocol version. + /// + /// + /// The protocol version. + /// + Version ProtocolVersion { get; } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/IHttpRequest.cs b/Vendor/EmbedIO-3.5.2/IHttpRequest.cs new file mode 100644 index 0000000..c4d6097 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/IHttpRequest.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Net; +using System.Text; + +namespace EmbedIO +{ + /// + /// + /// Interface to create a HTTP Request. + /// + public interface IHttpRequest : IHttpMessage + { + /// + /// Gets the request headers. + /// + NameValueCollection Headers { get; } + + /// + /// Gets a value indicating whether [keep alive]. + /// + bool KeepAlive { get; } + + /// + /// Gets the raw URL. + /// + string RawUrl { get; } + + /// + /// Gets the query string. + /// + NameValueCollection QueryString { get; } + + /// + /// Gets the HTTP method. + /// + string HttpMethod { get; } + + /// + /// Gets a constant representing the HTTP method of the request. + /// + HttpVerbs HttpVerb { get; } + + /// + /// Gets the URL. + /// + Uri Url { get; } + + /// + /// Gets a value indicating whether this instance has entity body. + /// + bool HasEntityBody { get; } + + /// + /// Gets the input stream. + /// + Stream InputStream { get; } + + /// + /// Gets the content encoding. + /// + Encoding ContentEncoding { get; } + + /// + /// Gets the remote end point. + /// + IPEndPoint RemoteEndPoint { get; } + + /// + /// Gets a value indicating whether this instance is local. + /// + bool IsLocal { get; } + + /// + /// Gets a value indicating whether this request has been received over a SSL connection. + /// + bool IsSecureConnection { get; } + + /// + /// Gets the user agent. + /// + string UserAgent { get; } + + /// + /// Gets a value indicating whether this instance is web socket request. + /// + bool IsWebSocketRequest { get; } + + /// + /// Gets the local end point. + /// + IPEndPoint LocalEndPoint { get; } + + /// + /// Gets the type of the content. + /// + string? ContentType { get; } + + /// + /// Gets the content length. + /// + long ContentLength64 { get; } + + /// + /// Gets a value indicating whether this instance is authenticated. + /// + bool IsAuthenticated { get; } + + /// + /// Gets the URL referrer. + /// + Uri? UrlReferrer { get; } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/IHttpResponse.cs b/Vendor/EmbedIO-3.5.2/IHttpResponse.cs new file mode 100644 index 0000000..48d8b21 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/IHttpResponse.cs @@ -0,0 +1,69 @@ +using System.IO; +using System.Net; +using System.Text; + +namespace EmbedIO +{ + /// + /// + /// Interface to create a HTTP Response. + /// + public interface IHttpResponse : IHttpMessage + { + /// + /// Gets the response headers. + /// + WebHeaderCollection Headers { get; } + + /// + /// Gets or sets the status code. + /// + int StatusCode { get; set; } + + /// + /// Gets or sets the content length. + /// + long ContentLength64 { get; set; } + + /// + /// Gets or sets the type of the content. + /// + string ContentType { get; set; } + + /// + /// Gets the output stream. + /// + Stream OutputStream { get; } + + /// + /// Gets or sets the content encoding. + /// + Encoding? ContentEncoding { get; set; } + + /// + /// Gets or sets a value indicating whether [keep alive]. + /// + bool KeepAlive { get; set; } + + /// + /// Gets or sets a value indicating whether the response uses chunked transfer encoding. + /// + bool SendChunked { get; set; } + + /// + /// Gets or sets a text description of the HTTP status code. + /// + string StatusDescription { get; set; } + + /// + /// Sets the cookie. + /// + /// The session cookie. + void SetCookie(Cookie cookie); + + /// + /// Closes this instance and dispose the resources. + /// + void Close(); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/IMimeTypeCustomizer.cs b/Vendor/EmbedIO-3.5.2/IMimeTypeCustomizer.cs new file mode 100644 index 0000000..ff9c70c --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/IMimeTypeCustomizer.cs @@ -0,0 +1,45 @@ +using System; + +namespace EmbedIO +{ + /// + /// Represents an object that can set information about specific MIME types and media ranges, + /// to be later retrieved via an interface. + /// + /// + public interface IMimeTypeCustomizer : IMimeTypeProvider + { + /// + /// Adds a custom association between a file extension and a MIME type. + /// + /// The file extension to associate to . + /// The MIME type to associate to . + /// The object implementing + /// has its configuration locked. + /// + /// is . + /// - or - + /// is . + /// + /// + /// is the empty string. + /// - or - + /// is not a valid MIME type. + /// + void AddCustomMimeType(string extension, string mimeType); + + /// + /// 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. + /// + /// The MIME type or media range. + /// to prefer compression; + /// otherwise, . + /// The object implementing + /// has its configuration locked. + /// is . + /// is not a valid MIME type or media range. + void PreferCompression(string mimeType, bool preferCompression); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/IMimeTypeProvider.cs b/Vendor/EmbedIO-3.5.2/IMimeTypeProvider.cs new file mode 100644 index 0000000..6d9458d --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/IMimeTypeProvider.cs @@ -0,0 +1,31 @@ +using System; + +namespace EmbedIO +{ + /// + /// Represents an object that contains information on specific MIME types and media ranges. + /// + public interface IMimeTypeProvider + { + /// + /// Gets the MIME type associated to a file extension. + /// + /// The file extension for which a corresponding MIME type is wanted. + /// The MIME type corresponding to , if one is found; + /// otherwise, . + /// is . + string GetMimeType(string extension); + + /// + /// Attempts to determine whether compression should be preferred + /// when negotiating content encoding for a response with the specified content type. + /// + /// The MIME type to check. + /// When this method returns , + /// a value indicating whether compression should be preferred. + /// This parameter is passed uninitialized. + /// if a value is found for ; + /// otherwise, . + bool TryDetermineCompression(string mimeType, out bool preferCompression); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/IWebModule.cs b/Vendor/EmbedIO-3.5.2/IWebModule.cs new file mode 100644 index 0000000..594f101 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/IWebModule.cs @@ -0,0 +1,81 @@ +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Routing; + +namespace EmbedIO +{ + /// + /// Represents a module. + /// + public interface IWebModule + { + /// + /// Gets the base route of a module. + /// + /// + /// The base route. + /// + /// + /// A base route is either "/" (the root path), + /// or a prefix starting and ending with a '/' character. + /// + string BaseRoute { get; } + + /// + /// Gets a value indicating whether processing of a request should stop + /// after a module has handled it. + /// + /// + /// If this property is , a HTTP context's + /// method will be automatically called + /// immediately after after the returned by + /// is completed. This will prevent + /// the context from being passed further along to other modules. + /// + /// + /// + bool IsFinalHandler { get; } + + /// + /// Gets or sets a callback that is called every time an unhandled exception + /// occurs during the processing of a request. + /// If this property is (the default), + /// the exception will be handled by the web server, or by the containing + /// . + /// + /// + ExceptionHandlerCallback? OnUnhandledException { get; set; } + + /// + /// Gets or sets a callback that is called every time a HTTP exception + /// is thrown during the processing of a request. + /// If this property is (the default), + /// the exception will be handled by the web server, or by the containing + /// . + /// + /// + HttpExceptionHandlerCallback? OnHttpException { get; set; } + + /// + /// Signals a module that the web server is starting. + /// + /// A used to stop the web server. + void Start(CancellationToken cancellationToken); + + /// + /// Matches the specified URL path against a module's , + /// extracting values for the route's parameters and a sub-path. + /// + /// The URL path to match. + /// If the match is successful, a object; + /// otherwise, . + RouteMatch MatchUrlPath(string urlPath); + + /// + /// Handles a request from a client. + /// + /// The context of the request being handled. + /// A representing the ongoing operation. + Task HandleRequestAsync(IHttpContext context); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/IWebModuleContainer.cs b/Vendor/EmbedIO-3.5.2/IWebModuleContainer.cs new file mode 100644 index 0000000..9e84235 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/IWebModuleContainer.cs @@ -0,0 +1,19 @@ +using System; +using EmbedIO.Utilities; + +namespace EmbedIO +{ + /// + /// Represents an object that contains a collection of interfaces. + /// + public interface IWebModuleContainer : IDisposable + { + /// + /// Gets the modules. + /// + /// + /// The modules. + /// + IComponentCollection Modules { get; } + } +} diff --git a/Vendor/EmbedIO-3.5.2/IWebServer.cs b/Vendor/EmbedIO-3.5.2/IWebServer.cs new file mode 100644 index 0000000..6bf0288 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/IWebServer.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Sessions; + +namespace EmbedIO +{ + /// + /// Represents a web server. + /// The basic usage of a web server is as follows: + /// + /// add modules to the Modules collection; + /// set a if needed; + /// call to respond to incoming requests. + /// + /// + public interface IWebServer : IWebModuleContainer, IMimeTypeCustomizer + { + /// + /// Occurs when the property changes. + /// + event WebServerStateChangedEventHandler StateChanged; + + /// + /// Gets or sets a callback that is called every time an unhandled exception + /// occurs during the processing of a request. + /// This property can never be . + /// If it is still + /// + /// + ExceptionHandlerCallback OnUnhandledException { get; set; } + + /// + /// Gets or sets a callback that is called every time a HTTP exception + /// is thrown during the processing of a request. + /// This property can never be . + /// + /// + HttpExceptionHandlerCallback OnHttpException { get; set; } + + /// + /// Gets or sets the registered session ID manager, if any. + /// A session ID manager is an implementation of . + /// Note that this property can only be set before starting the web server. + /// + /// + /// The session manager, or if no session manager is present. + /// + /// This property is being set and the web server has already been started. + ISessionManager? SessionManager { get; set; } + + /// + /// Gets the state of the web server. + /// + /// The state. + /// + WebServerState State { get; } + + /// + /// Starts the listener and the registered modules. + /// + /// The cancellation token; when cancelled, the server cancels all pending requests and stops. + /// + /// Returns the task that the HTTP listener is running inside of, so that it can be waited upon after it's been canceled. + /// + Task RunAsync(CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Internal/BufferingResponseStream.cs b/Vendor/EmbedIO-3.5.2/Internal/BufferingResponseStream.cs new file mode 100644 index 0000000..3323c6a --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Internal/BufferingResponseStream.cs @@ -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 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."); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Internal/CompressionStream.cs b/Vendor/EmbedIO-3.5.2/Internal/CompressionStream.cs new file mode 100644 index 0000000..b777566 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Internal/CompressionStream.cs @@ -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 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."); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Internal/CompressionUtility.cs b/Vendor/EmbedIO-3.5.2/Internal/CompressionUtility.cs new file mode 100644 index 0000000..58fe4ab --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Internal/CompressionUtility.cs @@ -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(); + } + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Internal/DummyWebModuleContainer.cs b/Vendor/EmbedIO-3.5.2/Internal/DummyWebModuleContainer.cs new file mode 100644 index 0000000..1ee1079 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Internal/DummyWebModuleContainer.cs @@ -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 Modules => throw UnexpectedCall(); + + public ConcurrentDictionary SharedItems => throw UnexpectedCall(); + + public void Dispose() + { + } + + private InternalErrorException UnexpectedCall([CallerMemberName] string member = "") + => SelfCheck.Failure($"Unexpected call to {nameof(DummyWebModuleContainer)}.{member}."); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Internal/LockableNameValueCollection.cs b/Vendor/EmbedIO-3.5.2/Internal/LockableNameValueCollection.cs new file mode 100644 index 0000000..75853f7 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Internal/LockableNameValueCollection.cs @@ -0,0 +1,9 @@ +using System.Collections.Specialized; + +namespace EmbedIO.Internal +{ + internal sealed class LockableNameValueCollection : NameValueCollection + { + public void MakeReadOnly() => IsReadOnly = true; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Internal/MimeTypeCustomizer.cs b/Vendor/EmbedIO-3.5.2/Internal/MimeTypeCustomizer.cs new file mode 100644 index 0000000..1d39224 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Internal/MimeTypeCustomizer.cs @@ -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 _customMimeTypes = new Dictionary(); + 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(); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Internal/RequestHandlerPassThroughException.cs b/Vendor/EmbedIO-3.5.2/Internal/RequestHandlerPassThroughException.cs new file mode 100644 index 0000000..c7be352 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Internal/RequestHandlerPassThroughException.cs @@ -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 +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Internal/TimeKeeper.cs b/Vendor/EmbedIO-3.5.2/Internal/TimeKeeper.cs new file mode 100644 index 0000000..03e9051 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Internal/TimeKeeper.cs @@ -0,0 +1,27 @@ +using System.Diagnostics; + +namespace EmbedIO.Internal +{ + /// + /// Represents a wrapper around Stopwatch. + /// + public sealed class TimeKeeper + { + private static readonly Stopwatch Stopwatch = Stopwatch.StartNew(); + + private readonly long _start; + + /// + /// Initializes a new instance of the class. + /// + public TimeKeeper() + { + _start = Stopwatch.ElapsedMilliseconds; + } + + /// + /// Gets the elapsed time since the class was initialized. + /// + public long ElapsedTime => Stopwatch.ElapsedMilliseconds - _start; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Internal/UriUtility.cs b/Vendor/EmbedIO-3.5.2/Internal/UriUtility.cs new file mode 100644 index 0000000..585ba42 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Internal/UriUtility.cs @@ -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 + }; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Internal/WebModuleCollection.cs b/Vendor/EmbedIO-3.5.2/Internal/WebModuleCollection.cs new file mode 100644 index 0000000..093fed3 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Internal/WebModuleCollection.cs @@ -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 + { + 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; + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/MimeType.Associations.cs b/Vendor/EmbedIO-3.5.2/MimeType.Associations.cs new file mode 100644 index 0000000..b78133f --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/MimeType.Associations.cs @@ -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. + // + // ------------------------------------------------------------------------------------------------- + + /// + /// Associates file extensions to MIME types. + /// + /// + /// The list of MIME types has been copied from + /// Samuel Neff's MIME Type Map + /// on April 26th, 2019. + /// Copyright (c) 2014 Samuel Neff. Redistributed under MIT license. + /// + public static IReadOnlyDictionary Associations { get; } = new Dictionary(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"}, + }; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/MimeType.cs b/Vendor/EmbedIO-3.5.2/MimeType.cs new file mode 100644 index 0000000..071407c --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/MimeType.cs @@ -0,0 +1,174 @@ +using System; +using EmbedIO.Utilities; + +namespace EmbedIO +{ + /// + /// Provides constants for commonly-used MIME types and association between file extensions and MIME types. + /// + /// + public static partial class MimeType + { + /// + /// The default MIME type for data whose type is unknown, + /// i.e. application/octet-stream. + /// + public const string Default = "application/octet-stream"; + + /// + /// The MIME type for plain text, i.e. text/plain. + /// + public const string PlainText = "text/plain"; + + /// + /// The MIME type for HTML, i.e. text/html. + /// + public const string Html = "text/html"; + + /// + /// The MIME type for JSON, i.e. application/json. + /// + public const string Json = "application/json"; + + /// + /// The MIME type for URL-encoded HTML forms, + /// i.e. application/x-www-form-urlencoded. + /// + internal const string UrlEncodedForm = "application/x-www-form-urlencoded"; + + /// + /// Strips parameters, if present (e.g. ; encoding=UTF-8), from a MIME type. + /// + /// The MIME type. + /// without parameters. + /// + /// This method does not validate : if it is not + /// a valid MIME type or media range, it is just returned unchanged. + /// + 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(); + } + + /// + /// Determines whether the specified string is a valid MIME type or media range. + /// + /// The value. + /// If set to , both media ranges + /// (e.g. "text/*", "*/*") and specific MIME types (e.g. "text/html") + /// are considered valid; if set to , only specific MIME types + /// are considered valid. + /// if is valid, + /// according to the value of ; + /// otherwise, . + 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); + } + + /// + /// Splits the specified MIME type or media range into type and subtype. + /// + /// The MIME type or media range to split. + /// A tuple of type and subtype. + /// is . + /// is not a valid + /// MIME type or media range. + public static (string type, string subtype) Split(string mimeType) + => UnsafeSplit(Validate.MimeType(nameof(mimeType), mimeType, true)); + + /// + /// Matches the specified MIME type to a media range. + /// + /// The MIME type to match. + /// The media range. + /// if is either + /// the same as , or has the same type and a subtype + /// of "*", or is "*/*". + /// + /// is . + /// - or - + /// is . + /// + /// + /// is not a valid MIME type. + /// - or - + /// is not a valid MIME media range. + /// + 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; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/MimeTypeCustomizerExtensions.cs b/Vendor/EmbedIO-3.5.2/MimeTypeCustomizerExtensions.cs new file mode 100644 index 0000000..1c8121d --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/MimeTypeCustomizerExtensions.cs @@ -0,0 +1,99 @@ +using System; + +namespace EmbedIO +{ + /// + /// Provides extension methods for types implementing . + /// + public static class MimeTypeCustomizerExtensions + { + /// + /// Adds a custom association between a file extension and a MIME type. + /// + /// The type of the object to which this method is applied. + /// The object to which this method is applied. + /// The file extension to associate to . + /// The MIME type to associate to . + /// with the custom association added. + /// is . + /// has its configuration locked. + /// + /// is . + /// - or - + /// is . + /// + /// + /// is the empty string. + /// - or - + /// is not a valid MIME type. + /// + public static T WithCustomMimeType(this T @this, string extension, string mimeType) + where T : IMimeTypeCustomizer + { + @this.AddCustomMimeType(extension, mimeType); + return @this; + } + + /// + /// 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. + /// + /// The type of the object to which this method is applied. + /// The object to which this method is applied. + /// The MIME type or media range. + /// to prefer compression; + /// otherwise, . + /// with the specified preference added. + /// is . + /// has its configuration locked. + /// is . + /// is not a valid MIME type or media range. + public static T PreferCompressionFor(this T @this, string mimeType, bool preferCompression) + where T : IMimeTypeCustomizer + { + @this.PreferCompression(mimeType, preferCompression); + return @this; + } + + /// + /// 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. + /// + /// The type of the object to which this method is applied. + /// The object to which this method is applied. + /// The MIME type or media range. + /// with the specified preference added. + /// is . + /// has its configuration locked. + /// is . + /// is not a valid MIME type or media range. + public static T PreferCompressionFor(this T @this, string mimeType) + where T : IMimeTypeCustomizer + { + @this.PreferCompression(mimeType, true); + return @this; + } + + /// + /// 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. + /// + /// The type of the object to which this method is applied. + /// The object to which this method is applied. + /// The MIME type or media range. + /// with the specified preference added. + /// is . + /// has its configuration locked. + /// is . + /// is not a valid MIME type or media range. + public static T PreferNoCompressionFor(this T @this, string mimeType) + where T : IMimeTypeCustomizer + { + @this.PreferCompression(mimeType, false); + return @this; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/ModuleGroup.cs b/Vendor/EmbedIO-3.5.2/ModuleGroup.cs new file mode 100644 index 0000000..ad4db32 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/ModuleGroup.cs @@ -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 +{ + /// + /// Groups modules under a common base URL path. + /// The BaseRoute property + /// of modules contained in a ModuleGroup is relative to the + /// ModuleGroup's BaseRoute property. + /// For example, given the following code: + /// new ModuleGroup("/download") + /// .WithStaticFilesAt("/docs", "/var/my/documents"); + /// files contained in the /var/my/documents folder will be + /// available to clients under the /download/docs/ URL. + /// + /// + /// + /// + public class ModuleGroup : WebModuleBase, IDisposable, IWebModuleContainer, IMimeTypeCustomizer + { + private readonly WebModuleCollection _modules; + private readonly MimeTypeCustomizer _mimeTypeCustomizer = new MimeTypeCustomizer(); + + /// + /// Initializes a new instance of the class. + /// + /// The base route served by this module. + /// The value to set the property to. + /// See the help for the property for more information. + /// + /// + public ModuleGroup(string baseRoute, bool isFinalHandler) + : base(baseRoute) + { + IsFinalHandler = isFinalHandler; + _modules = new WebModuleCollection(nameof(ModuleGroup)); + } + + /// + /// Finalizes an instance of the class. + /// + ~ModuleGroup() + { + Dispose(false); + } + + /// + public sealed override bool IsFinalHandler { get; } + + /// + public IComponentCollection Modules => _modules; + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + string IMimeTypeProvider.GetMimeType(string extension) + => _mimeTypeCustomizer.GetMimeType(extension); + + bool IMimeTypeProvider.TryDetermineCompression(string mimeType, out bool preferCompression) + => _mimeTypeCustomizer.TryDetermineCompression(mimeType, out preferCompression); + + /// + public void AddCustomMimeType(string extension, string mimeType) + => _mimeTypeCustomizer.AddCustomMimeType(extension, mimeType); + + /// + public void PreferCompression(string mimeType, bool preferCompression) + => _mimeTypeCustomizer.PreferCompression(mimeType, preferCompression); + + /// + protected override Task OnRequestAsync(IHttpContext context) + => _modules.DispatchRequestAsync(context); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!disposing) + return; + + _modules.Dispose(); + } + + /// + protected override void OnBeforeLockConfiguration() + { + base.OnBeforeLockConfiguration(); + + _mimeTypeCustomizer.Lock(); + } + + /// + protected override void OnStart(CancellationToken cancellationToken) + { + _modules.StartAll(cancellationToken); + } + } +} diff --git a/Vendor/EmbedIO-3.5.2/Net/CookieList.cs b/Vendor/EmbedIO-3.5.2/Net/CookieList.cs new file mode 100644 index 0000000..75bf4a4 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/CookieList.cs @@ -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 +{ + /// + /// Provides a collection container for instances of . + /// This class is meant to be used internally by EmbedIO; you don't need to + /// use this class directly. + /// +#pragma warning disable CA1710 // Rename class to end in 'Collection' - it ends in 'List', i.e. 'Indexed Collection'. + public sealed class CookieList : List, ICookieCollection +#pragma warning restore CA1710 + { + /// + public bool IsSynchronized => false; + + /// + public Cookie? this[string name] + { + get + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (Count == 0) + return null; + + var list = new List(this); + + list.Sort(CompareCookieWithinSorted); + + return list.FirstOrDefault(cookie => cookie.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + } + } + + /// Creates a by parsing + /// the value of one or more Cookie or Set-Cookie headers. + /// The value, or comma-separated list of values, + /// of the header or headers. + /// A newly-created instance of . + 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; + } + + /// + 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; + } + + /// + 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; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/EndPointManager.cs b/Vendor/EmbedIO-3.5.2/Net/EndPointManager.cs new file mode 100644 index 0000000..2103312 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/EndPointManager.cs @@ -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 +{ + /// + /// Represents the EndPoint Manager. + /// + public static class EndPointManager + { + private static readonly ConcurrentDictionary> IPToEndpoints = new (); + + /// + /// Gets or sets a value indicating whether [use IPv6]. By default, this flag is set. + /// + /// + /// true if [use IPv6]; otherwise, false. + /// + public static bool UseIpv6 { get; set; } = true; + + internal static void AddListener(HttpListener listener) + { + var added = new List(); + + 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()); + 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 + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/HttpListener.cs b/Vendor/EmbedIO-3.5.2/Net/HttpListener.cs new file mode 100644 index 0000000..2f96ffb --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/HttpListener.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Net.Internal; + +namespace EmbedIO.Net +{ + /// + /// The EmbedIO implementation of the standard HTTP Listener class. + /// + /// Based on MONO HttpListener class. + /// + /// + public sealed class HttpListener : IHttpListener + { + private readonly SemaphoreSlim _ctxQueueSem = new (0); + private readonly ConcurrentDictionary _ctxQueue; + private readonly ConcurrentDictionary _connections; + private readonly HttpListenerPrefixCollection _prefixes; + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The certificate. + public HttpListener(X509Certificate? certificate = null) + { + Certificate = certificate; + + _prefixes = new HttpListenerPrefixCollection(this); + _connections = new ConcurrentDictionary(); + _ctxQueue = new ConcurrentDictionary(); + } + + /// + public bool IgnoreWriteExceptions { get; set; } = true; + + /// + public bool IsListening { get; private set; } + + /// + public string Name { get; } = "Unosquare HTTP Listener"; + + /// + public List Prefixes => _prefixes.ToList(); + + /// + /// Gets the certificate. + /// + /// + /// The certificate. + /// + internal X509Certificate? Certificate { get; } + + /// + public void Start() + { + if (IsListening) + { + return; + } + + EndPointManager.AddListener(this); + IsListening = true; + } + + /// + public void Stop() + { + IsListening = false; + Close(false); + } + + /// + public void AddPrefix(string urlPrefix) => _prefixes.Add(urlPrefix); + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + Close(true); + _ctxQueueSem.Dispose(); + _disposed = true; + } + + /// + public async Task GetContextAsync(CancellationToken cancellationToken) + { + while (true) + { + await _ctxQueueSem.WaitAsync(cancellationToken).ConfigureAwait(false); + + foreach (var key in _ctxQueue.Keys) + { + if (_ctxQueue.TryRemove(key, out var context)) + { + return context; + } + + break; + } + } + } + + internal void RegisterContext(HttpListenerContext context) + { + if (!_ctxQueue.TryAdd(context.Id, context)) + { + throw new InvalidOperationException("Unable to register context"); + } + + _ = _ctxQueueSem.Release(); + } + + internal void UnregisterContext(HttpListenerContext context) => _ctxQueue.TryRemove(context.Id, out _); + + internal void AddConnection(HttpConnection cnc) => _connections[cnc] = cnc; + + internal void RemoveConnection(HttpConnection cnc) => _connections.TryRemove(cnc, out _); + + private void Close(bool closeExisting) + { + EndPointManager.RemoveListener(this); + + var keys = _connections.Keys; + var connections = new HttpConnection[keys.Count]; + keys.CopyTo(connections, 0); + _connections.Clear(); + var list = new List(connections); + + for (var i = list.Count - 1; i >= 0; i--) + { + list[i].Close(true); + } + + if (!closeExisting) + { + return; + } + + while (!_ctxQueue.IsEmpty) + { + foreach (var key in _ctxQueue.Keys.ToArray()) + { + if (_ctxQueue.TryGetValue(key, out var context)) + { + context.Connection.Close(true); + } + } + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/EndPointListener.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/EndPointListener.cs new file mode 100644 index 0000000..caab9c7 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/EndPointListener.cs @@ -0,0 +1,429 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace EmbedIO.Net.Internal +{ + internal sealed class EndPointListener : IDisposable + { + private readonly Dictionary _unregistered; + private readonly IPEndPoint _endpoint; + private readonly Socket _sock; + private Dictionary _prefixes; + private List? _unhandled; // unhandled; host = '*' + private List? _all; // all; host = '+ + + public EndPointListener(HttpListener listener, IPAddress address, int port, bool secure) + { + Listener = listener; + Secure = secure; + _endpoint = new IPEndPoint(address, port); + _sock = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + if (address.AddressFamily == AddressFamily.InterNetworkV6 && EndPointManager.UseIpv6) + { + _sock.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false); + } + + _sock.Bind(_endpoint); + _sock.Listen(500); + var args = new SocketAsyncEventArgs { UserToken = this }; + args.Completed += OnAccept; + Socket? dummy = null; + Accept(_sock, args, ref dummy); + _prefixes = new Dictionary(); + _unregistered = new Dictionary(); + } + + internal HttpListener Listener { get; } + + internal bool Secure { get; } + + public bool BindContext(HttpListenerContext context) + { + var req = context.Request; + var listener = SearchListener(req.Url, out var prefix); + + if (listener == null) + { + return false; + } + + context.Listener = listener; + context.Connection.Prefix = prefix; + return true; + } + + public void UnbindContext(HttpListenerContext context) => context.Listener.UnregisterContext(context); + + public void Dispose() + { + _sock.Dispose(); + List connections; + + lock (_unregistered) + { + // Clone the list because RemoveConnection can be called from Close + connections = new List(_unregistered.Keys); + _unregistered.Clear(); + } + + foreach (var c in connections) + { + c.Dispose(); + } + } + + public void AddPrefix(ListenerPrefix prefix, HttpListener listener) + { + List? current; + List future; + + if (prefix.Host == "*") + { + do + { + current = _unhandled; + + // TODO: Should we clone the items? + future = current?.ToList() ?? new List(); + prefix.Listener = listener; + AddSpecial(future, prefix); + } + while (Interlocked.CompareExchange(ref _unhandled, future, current) != current); + + return; + } + + if (prefix.Host == "+") + { + do + { + current = _all; + future = current?.ToList() ?? new List(); + prefix.Listener = listener; + AddSpecial(future, prefix); + } + while (Interlocked.CompareExchange(ref _all, future, current) != current); + return; + } + + Dictionary prefs, p2; + + do + { + prefs = _prefixes; + if (prefs.ContainsKey(prefix)) + { + if (prefs[prefix] != listener) + { + throw new HttpListenerException(400, $"There is another listener for {prefix}"); + } + + return; + } + + p2 = prefs.ToDictionary(x => x.Key, x => x.Value); + p2[prefix] = listener; + } + while (Interlocked.CompareExchange(ref _prefixes, p2, prefs) != prefs); + } + + public void RemovePrefix(ListenerPrefix prefix) + { + List? current; + List future; + + if (prefix.Host == "*") + { + do + { + current = _unhandled; + future = current?.ToList() ?? new List(); + if (!RemoveSpecial(future, prefix)) + { + break; // Prefix not found + } + } + while (Interlocked.CompareExchange(ref _unhandled, future, current) != current); + + CheckIfRemove(); + return; + } + + if (prefix.Host == "+") + { + do + { + current = _all; + future = current?.ToList() ?? new List(); + if (!RemoveSpecial(future, prefix)) + { + break; // Prefix not found + } + } + while (Interlocked.CompareExchange(ref _all, future, current) != current); + + CheckIfRemove(); + return; + } + + Dictionary prefs, p2; + + do + { + prefs = _prefixes; + var prefixKey = _prefixes.Keys.FirstOrDefault(p => p.Path == prefix.Path); + + if (prefixKey is null) + { + break; + } + + p2 = prefs.ToDictionary(x => x.Key, x => x.Value); + _ = p2.Remove(prefixKey); + } + while (Interlocked.CompareExchange(ref _prefixes, p2, prefs) != prefs); + + CheckIfRemove(); + } + + internal void RemoveConnection(HttpConnection conn) + { + lock (_unregistered) + { + _ = _unregistered.Remove(conn); + } + } + + private static void Accept(Socket socket, SocketAsyncEventArgs e, ref Socket? accepted) + { + e.AcceptSocket = null; + bool acceptPending; + + try + { + acceptPending = socket.AcceptAsync(e); + } + catch + { + try + { + accepted?.Dispose(); + } + catch + { + // ignored + } + + accepted = null; + return; + } + + if (!acceptPending) + { + ProcessAccept(e); + } + } + + private static void ProcessAccept(SocketAsyncEventArgs args) + { + Socket? accepted = null; + if (args.SocketError == SocketError.Success) + { + accepted = args.AcceptSocket; + } + + var epl = (EndPointListener)args.UserToken; + + Accept(epl._sock, args, ref accepted); + if (accepted == null) + { + return; + } + + if (epl.Secure && epl.Listener.Certificate == null) + { + accepted.Dispose(); + return; + } + + HttpConnection conn; + try + { + conn = new HttpConnection(accepted, epl); + } + catch + { + return; + } + + lock (epl._unregistered) + { + epl._unregistered[conn] = conn; + } + + _ = conn.BeginReadRequest(); + } + + private static void OnAccept(object sender, SocketAsyncEventArgs e) => ProcessAccept(e); + + private static HttpListener? MatchFromList(string path, List? list, out ListenerPrefix? prefix) + { + prefix = null; + if (list == null) + { + return null; + } + + HttpListener? bestMatch = null; + var bestLength = -1; + + foreach (var p in list) + { + if (p.Path.Length < bestLength || !path.StartsWith(p.Path, StringComparison.Ordinal)) + { + continue; + } + + bestLength = p.Path.Length; + bestMatch = p.Listener; + prefix = p; + } + + return bestMatch; + } + + private static void AddSpecial(ICollection coll, ListenerPrefix prefix) + { + if (coll == null) + { + return; + } + + if (coll.Any(p => p.Path == prefix.Path)) + { + throw new HttpListenerException(400, "Prefix already in use."); + } + + coll.Add(prefix); + } + + private static bool RemoveSpecial(IList coll, ListenerPrefix prefix) + { + if (coll == null) + { + return false; + } + + var c = coll.Count; + for (var i = 0; i < c; i++) + { + if (coll[i].Path != prefix.Path) + { + continue; + } + + coll.RemoveAt(i); + return true; + } + + return false; + } + + private HttpListener? SearchListener(Uri uri, out ListenerPrefix? prefix) + { + prefix = null; + if (uri == null) + { + return null; + } + + var host = uri.Host; + var port = uri.Port; + var path = WebUtility.UrlDecode(uri.AbsolutePath); + var pathSlash = path[path.Length - 1] == '/' ? path : path + "/"; + + HttpListener? bestMatch = null; + var bestLength = -1; + + if (!string.IsNullOrEmpty(host)) + { + var result = _prefixes; + + foreach (var p in result.Keys) + { + if (p.Path.Length < bestLength) + { + continue; + } + + if (p.Host != host || p.Port != port) + { + continue; + } + + if (!path.StartsWith(p.Path, StringComparison.Ordinal) && !pathSlash.StartsWith(p.Path, StringComparison.Ordinal)) + { + continue; + } + + bestLength = p.Path.Length; + bestMatch = result[p]; + prefix = p; + } + + if (bestLength != -1) + { + return bestMatch; + } + } + + var list = _unhandled; + bestMatch = MatchFromList(path, list, out prefix); + if (path != pathSlash && bestMatch == null) + { + bestMatch = MatchFromList(pathSlash, list, out prefix); + } + + if (bestMatch != null) + { + return bestMatch; + } + + list = _all; + bestMatch = MatchFromList(path, list, out prefix); + if (path != pathSlash && bestMatch == null) + { + bestMatch = MatchFromList(pathSlash, list, out prefix); + } + + return bestMatch; + } + + private void CheckIfRemove() + { + if (_prefixes.Count > 0) + { + return; + } + + var list = _unhandled; + if (list != null && list.Count > 0) + { + return; + } + + list = _all; + if (list != null && list.Count > 0) + { + return; + } + + EndPointManager.RemoveEndPoint(this, _endpoint); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/HeaderUtility.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/HeaderUtility.cs new file mode 100644 index 0000000..6e24e11 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/HeaderUtility.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq; + +namespace EmbedIO.Net.Internal +{ + internal static class HeaderUtility + { + public static string? GetCharset(string? contentType) + => contentType? + .Split(';') + .Select(p => p.Trim()) + .Where(part => part.StartsWith("charset", StringComparison.OrdinalIgnoreCase)) + .Select(GetAttributeValue) + .FirstOrDefault(); + + public static string? GetAttributeValue(string nameAndValue) + { + var idx = nameAndValue.IndexOf('='); + + return idx < 0 || idx == nameAndValue.Length - 1 ? null : nameAndValue.Substring(idx + 1).Trim().Unquote(); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/HttpConnection.InputState.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpConnection.InputState.cs new file mode 100644 index 0000000..57bc2c5 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpConnection.InputState.cs @@ -0,0 +1,11 @@ +namespace EmbedIO.Net.Internal +{ + partial class HttpConnection + { + private enum InputState + { + RequestLine, + Headers, + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/HttpConnection.LineState.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpConnection.LineState.cs new file mode 100644 index 0000000..4a3feca --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpConnection.LineState.cs @@ -0,0 +1,12 @@ +namespace EmbedIO.Net.Internal +{ + partial class HttpConnection + { + private enum LineState + { + None, + Cr, + Lf, + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/HttpConnection.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpConnection.cs new file mode 100644 index 0000000..230a46d --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpConnection.cs @@ -0,0 +1,416 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EmbedIO.Net.Internal +{ + internal sealed partial class HttpConnection : IDisposable + { + private const int BufferSize = 8192; + + private readonly Timer _timer; + private readonly EndPointListener _epl; + private Socket? _sock; + private MemoryStream? _ms; + private byte[]? _buffer; + private HttpListenerContext _context; + private StringBuilder? _currentLine; + private RequestStream? _iStream; + private ResponseStream? _oStream; + private bool _contextBound; + private int _sTimeout = 90000; // 90k ms for first request, 15k ms from then on + private HttpListener? _lastListener; + private InputState _inputState = InputState.RequestLine; + private LineState _lineState = LineState.None; + private int _position; + private string? _errorMessage; + + public HttpConnection(Socket sock, EndPointListener epl) + { + _sock = sock; + _epl = epl; + IsSecure = epl.Secure; + LocalEndPoint = (IPEndPoint) sock.LocalEndPoint; + RemoteEndPoint = (IPEndPoint) sock.RemoteEndPoint; + + Stream = new NetworkStream(sock, false); + if (IsSecure) + { + var sslStream = new SslStream(Stream, true); + + try + { + sslStream.AuthenticateAsServer(epl.Listener.Certificate); + } + catch + { + CloseSocket(); + throw; + } + + Stream = sslStream; + } + + _timer = new Timer(OnTimeout, null, Timeout.Infinite, Timeout.Infinite); + _context = null!; // Silence warning about uninitialized field - _context will be initialized by the Init method + Init(); + } + + public int Reuses { get; private set; } + + public Stream Stream { get; } + + public IPEndPoint LocalEndPoint { get; } + + public IPEndPoint RemoteEndPoint { get; } + + public bool IsSecure { get; } + + public ListenerPrefix? Prefix { get; set; } + + public void Dispose() + { + Close(true); + + _timer.Dispose(); + _sock?.Dispose(); + _ms?.Dispose(); + _iStream?.Dispose(); + _oStream?.Dispose(); + Stream?.Dispose(); + _lastListener?.Dispose(); + } + + public async Task BeginReadRequest() + { + _buffer ??= new byte[BufferSize]; + + try + { + if (Reuses == 1) + { + _sTimeout = 15000; + } + + _ = _timer.Change(_sTimeout, Timeout.Infinite); + + var data = await Stream.ReadAsync(_buffer, 0, BufferSize).ConfigureAwait(false); + await OnReadInternal(data).ConfigureAwait(false); + } + catch + { + _ = _timer.Change(Timeout.Infinite, Timeout.Infinite); + CloseSocket(); + Unbind(); + } + } + + public RequestStream GetRequestStream(long contentLength) + { + if (_iStream == null) + { + var buffer = _ms.ToArray(); + var length = (int) _ms.Length; + _ms = null; + + _iStream = new RequestStream(Stream, buffer, _position, length - _position, contentLength); + } + + return _iStream; + } + + public ResponseStream GetResponseStream() => _oStream ??= new ResponseStream(Stream, _context.HttpListenerResponse, _context.Listener?.IgnoreWriteExceptions ?? true); + + internal void SetError(string message) => _errorMessage = message; + + internal void ForceClose() => Close(true); + + internal void Close(bool forceClose = false) + { + if (_sock != null) + { + _oStream?.Dispose(); + _oStream = null; + } + + if (_sock == null) + { + return; + } + + forceClose = forceClose + || !_context.Request.KeepAlive + || _context.Response.Headers["connection"] == "close"; + + if (!forceClose) + { + if (_context.HttpListenerRequest.FlushInput()) + { + Reuses++; + Unbind(); + Init(); + _ = BeginReadRequest(); + return; + } + } + + using (var s = _sock) + { + _sock = null; + try + { + s?.Shutdown(SocketShutdown.Both); + } + catch + { + // ignored + } + } + + Unbind(); + RemoveConnection(); + } + + private void Init() + { + _contextBound = false; + _iStream = null; + _oStream = null; + Prefix = null; + _ms = new MemoryStream(); + _position = 0; + _inputState = InputState.RequestLine; + _lineState = LineState.None; + _context = new HttpListenerContext(this); + } + + private void OnTimeout(object unused) + { + CloseSocket(); + Unbind(); + } + + private async Task OnReadInternal(int offset) + { + _ = _timer.Change(Timeout.Infinite, Timeout.Infinite); + + // Continue reading until full header is received. + // Especially important for multipart requests when the second part of the header arrives after a tiny delay + // because the web browser has to measure the content length first. + while (true) + { + try + { + await _ms.WriteAsync(_buffer, 0, offset).ConfigureAwait(false); + if (_ms.Length > 32768) + { + Close(true); + return; + } + } + catch + { + CloseSocket(); + Unbind(); + return; + } + + if (offset == 0) + { + CloseSocket(); + Unbind(); + return; + } + + if (ProcessInput(_ms)) + { + if (_errorMessage is null) + { + _context.HttpListenerRequest.FinishInitialization(); + } + + if (_errorMessage != null || !_epl.BindContext(_context)) + { + Close(true); + return; + } + + var listener = _context.Listener; + if (_lastListener != listener) + { + RemoveConnection(); + listener.AddConnection(this); + _lastListener = listener; + } + + _contextBound = true; + listener.RegisterContext(_context); + return; + } + + offset = await Stream.ReadAsync(_buffer, 0, BufferSize).ConfigureAwait(false); + } + } + + private void RemoveConnection() + { + if (_lastListener != null) + { + _lastListener.RemoveConnection(this); + } + else + { + _epl.RemoveConnection(this); + } + } + + // true -> done processing + // false -> need more input + private bool ProcessInput(MemoryStream ms) + { + var buffer = ms.ToArray(); + var len = (int)ms.Length; + var used = 0; + + while (true) + { + if (_errorMessage != null) + { + return true; + } + + if (_position >= len) + { + break; + } + + string? line; + try + { + line = ReadLine(buffer, _position, len - _position, out used); + _position += used; + } + catch + { + _errorMessage = "Bad request"; + return true; + } + + if (line == null) + { + break; + } + + if (string.IsNullOrEmpty(line)) + { + if (_inputState == InputState.RequestLine) + { + continue; + } + + _currentLine = null; + + return true; + } + + if (_inputState == InputState.RequestLine) + { + _context.HttpListenerRequest.SetRequestLine(line); + _inputState = InputState.Headers; + } + else + { + try + { + _context.HttpListenerRequest.AddHeader(line); + } + catch (Exception e) + { + _errorMessage = e.Message; + return true; + } + } + } + + if (used == len) + { + ms.SetLength(0); + _position = 0; + } + + return false; + } + + private string? ReadLine(byte[] buffer, int offset, int len, out int used) + { + _currentLine ??= new StringBuilder(128); + + var last = offset + len; + used = 0; + for (var i = offset; i < last && _lineState != LineState.Lf; i++) + { + used++; + var b = buffer[i]; + + switch (b) + { + case 13: + _lineState = LineState.Cr; + break; + case 10: + _lineState = LineState.Lf; + break; + default: + _ = _currentLine.Append((char)b); + break; + } + } + + if (_lineState != LineState.Lf) + { + return null; + } + + _lineState = LineState.None; + var result = _currentLine.ToString(); + _currentLine.Length = 0; + return result; + } + + private void Unbind() + { + if (!_contextBound) + { + return; + } + + _epl.UnbindContext(_context); + _contextBound = false; + } + + private void CloseSocket() + { + if (_sock == null) + { + return; + } + + try + { + _sock.Dispose(); + } + finally + { + _sock = null; + } + + RemoveConnection(); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/HttpListenerContext.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpListenerContext.cs new file mode 100644 index 0000000..134493c --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpListenerContext.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Authentication; +using EmbedIO.Internal; +using EmbedIO.Routing; +using EmbedIO.Sessions; +using EmbedIO.Utilities; +using EmbedIO.WebSockets; +using EmbedIO.WebSockets.Internal; +using Swan.Logging; + +namespace EmbedIO.Net.Internal +{ + // Provides access to the request and response objects used by the HttpListener class. + internal sealed class HttpListenerContext : IHttpContextImpl + { + private readonly Lazy> _items = new (() => new Dictionary(), true); + + private readonly TimeKeeper _ageKeeper = new (); + + private readonly Stack> _closeCallbacks = new (); + + private bool _closed; + + internal HttpListenerContext(HttpConnection cnc) + { + Connection = cnc; + HttpListenerRequest = new HttpListenerRequest(this); + User = Auth.NoUser; + HttpListenerResponse = new HttpListenerResponse(this); + Id = UniqueIdGenerator.GetNext(); + LocalEndPoint = Request.LocalEndPoint; + RemoteEndPoint = Request.RemoteEndPoint; + Route = RouteMatch.None; + Session = SessionProxy.None; + } + + public string Id { get; } + + public CancellationToken CancellationToken { get; set; } + + public long Age => _ageKeeper.ElapsedTime; + + public IPEndPoint LocalEndPoint { get; } + + public IPEndPoint RemoteEndPoint { get; } + + public IHttpRequest Request => HttpListenerRequest; + + public RouteMatch Route { get; set; } + + public string RequestedPath => Route.SubPath ?? string.Empty; // It will never be empty, because modules are matched via base routes - this is just to silence a warning. + + public IHttpResponse Response => HttpListenerResponse; + + public IPrincipal User { get; set; } + + public ISessionProxy Session { get; set; } + + public bool SupportCompressedRequests { get; set; } + + public IDictionary Items => _items.Value; + + public bool IsHandled { get; private set; } + + public MimeTypeProviderStack MimeTypeProviders { get; } = new MimeTypeProviderStack(); + + internal HttpListenerRequest HttpListenerRequest { get; } + + internal HttpListenerResponse HttpListenerResponse { get; } + + internal HttpListener? Listener { get; set; } + + internal HttpConnection Connection { get; } + + public void SetHandled() => IsHandled = true; + + public void OnClose(Action callback) + { + if (_closed) + { + throw new InvalidOperationException("HTTP context has already been closed."); + } + + _closeCallbacks.Push(Validate.NotNull(nameof(callback), callback)); + } + + public void Close() + { + _closed = true; + + // Always close the response stream no matter what. + Response.Close(); + + foreach (var callback in _closeCallbacks) + { + try + { + callback(this); + } + catch (Exception e) + { + e.Log("HTTP context", $"[{Id}] Exception thrown by a HTTP context close callback."); + } + } + } + + public async Task AcceptWebSocketAsync( + IEnumerable requestedProtocols, + string acceptedProtocol, + int receiveBufferSize, + TimeSpan keepAliveInterval, + CancellationToken cancellationToken) + { + var webSocket = await WebSocket.AcceptAsync(this, acceptedProtocol).ConfigureAwait(false); + return new WebSocketContext(this, WebSocket.SupportedVersion, requestedProtocols, acceptedProtocol, webSocket, cancellationToken); + } + + public string GetMimeType(string extension) + => MimeTypeProviders.GetMimeType(extension); + + public bool TryDetermineCompression(string mimeType, out bool preferCompression) + => MimeTypeProviders.TryDetermineCompression(mimeType, out preferCompression); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/HttpListenerPrefixCollection.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpListenerPrefixCollection.cs new file mode 100644 index 0000000..6248b6f --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpListenerPrefixCollection.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace EmbedIO.Net.Internal +{ + internal class HttpListenerPrefixCollection : List + { + private readonly HttpListener _listener; + + internal HttpListenerPrefixCollection(HttpListener listener) + { + _listener = listener; + } + + public new void Add(string uriPrefix) + { + ListenerPrefix.CheckUri(uriPrefix); + if (Contains(uriPrefix)) + { + return; + } + + base.Add(uriPrefix); + + if (_listener.IsListening) + { + EndPointManager.AddPrefix(uriPrefix, _listener); + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/HttpListenerRequest.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpListenerRequest.cs new file mode 100644 index 0000000..548f44d --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpListenerRequest.cs @@ -0,0 +1,497 @@ +using System; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using EmbedIO.Internal; +using EmbedIO.Utilities; + +namespace EmbedIO.Net.Internal +{ + /// + /// Represents an HTTP Listener Request. + /// + internal sealed partial class HttpListenerRequest : IHttpRequest + { + private static readonly byte[] HttpStatus100 = WebServer.DefaultEncoding.GetBytes("HTTP/1.1 100 Continue\r\n\r\n"); + private static readonly char[] Separators = { ' ' }; + + private readonly HttpConnection _connection; + private CookieList? _cookies; + private Stream? _inputStream; + private bool _kaSet; + private bool _keepAlive; + + internal HttpListenerRequest(HttpListenerContext context) + { + _connection = context.Connection; + } + + /// + /// Gets the MIME accept types. + /// + /// + /// The accept types. + /// + public string[] AcceptTypes { get; private set; } = Array.Empty(); + + /// + public Encoding ContentEncoding + { + get + { + if (!HasEntityBody || ContentType == null) + { + return WebServer.DefaultEncoding; + } + + var charSet = HeaderUtility.GetCharset(ContentType); + if (string.IsNullOrEmpty(charSet)) + { + return WebServer.DefaultEncoding; + } + + try + { + return Encoding.GetEncoding(charSet); + } + catch (ArgumentException) + { + return WebServer.DefaultEncoding; + } + } + } + + /// + public long ContentLength64 => long.TryParse(Headers[HttpHeaderNames.ContentLength], out var val) ? val : 0; + + /// + public string ContentType => Headers[HttpHeaderNames.ContentType]; + + /// + public ICookieCollection Cookies => _cookies ??= new CookieList(); + + /// + public bool HasEntityBody => ContentLength64 > 0; + + /// + public NameValueCollection Headers { get; } = new (); + + /// + public string HttpMethod { get; private set; } = string.Empty; + + /// + public HttpVerbs HttpVerb { get; private set; } + + /// + public Stream InputStream => _inputStream ??= ContentLength64 > 0 ? _connection.GetRequestStream(ContentLength64) : Stream.Null; + + /// + public bool IsAuthenticated => false; + + /// + public bool IsLocal => LocalEndPoint.Address?.Equals(RemoteEndPoint.Address) ?? true; + + /// + public bool IsSecureConnection => _connection.IsSecure; + + /// + public bool KeepAlive + { + get + { + if (!_kaSet) + { + var cnc = Headers.GetValues(HttpHeaderNames.Connection); + _keepAlive = ProtocolVersion < HttpVersion.Version11 + ? cnc != null && cnc.Length == 1 && string.Compare(cnc[0], "keep-alive", StringComparison.OrdinalIgnoreCase) == 0 + : cnc == null || cnc.All(s => string.Compare(s, "close", StringComparison.OrdinalIgnoreCase) != 0); + + _kaSet = true; + } + + return _keepAlive; + } + } + + /// + public IPEndPoint LocalEndPoint => _connection.LocalEndPoint; + + /// + public Version ProtocolVersion { get; private set; } = HttpVersion.Version11; + + /// + public NameValueCollection QueryString { get; } = new (); + + /// + public string RawUrl { get; private set; } = string.Empty; + + /// + public IPEndPoint RemoteEndPoint => _connection.RemoteEndPoint; + + /// + public Uri Url { get; private set; } = WebServer.NullUri; + + /// + public Uri? UrlReferrer { get; private set; } + + /// + public string UserAgent => Headers[HttpHeaderNames.UserAgent]; + + public string UserHostAddress => LocalEndPoint.ToString(); + + public string UserHostName => Headers[HttpHeaderNames.Host]; + + public string[] UserLanguages { get; private set; } = Array.Empty(); + + /// + public bool IsWebSocketRequest + => HttpVerb == HttpVerbs.Get + && ProtocolVersion >= HttpVersion.Version11 + && Headers.Contains(HttpHeaderNames.Upgrade, "websocket") + && Headers.Contains(HttpHeaderNames.Connection, "Upgrade"); + + internal void SetRequestLine(string req) + { + const string forbiddenMethodChars = "\"(),/:;<=>?@[\\]{}"; + + var parts = req.Split(Separators, 3); + if (parts.Length != 3) + { + _connection.SetError("Invalid request line (parts)."); + return; + } + + HttpMethod = parts[0]; + foreach (var c in HttpMethod) + { + // See https://tools.ietf.org/html/rfc7230#section-3.2.6 + // for the list of allowed characters + if (c < 32 || c >= 127 || forbiddenMethodChars.IndexOf(c) >= 0) + { + _connection.SetError("(Invalid verb)"); + return; + } + } + + HttpVerb = IsKnownHttpMethod(HttpMethod, out var verb) ? verb : HttpVerbs.Any; + + RawUrl = parts[1]; + if (parts[2].Length != 8 || !parts[2].StartsWith("HTTP/", StringComparison.Ordinal)) + { + _connection.SetError("Invalid request line (missing HTTP version)."); + return; + } + + try + { + ProtocolVersion = new Version(parts[2].Substring(5)); + + if (ProtocolVersion.Major < 1) + { + throw new InvalidOperationException(); + } + } + catch + { + _connection.SetError("Invalid request line (could not parse HTTP version)."); + } + } + + internal void FinishInitialization() + { + var host = UserHostName; + if (ProtocolVersion > HttpVersion.Version10 && string.IsNullOrEmpty(host)) + { + _connection.SetError("Invalid host name"); + return; + } + + var rawUri = UriUtility.StringToAbsoluteUri(RawUrl.ToLowerInvariant()); + var path = rawUri?.PathAndQuery ?? RawUrl; + + if (string.IsNullOrEmpty(host)) + { + host = rawUri?.Host ?? UserHostAddress; + } + + var colonPos = host.LastIndexOf(':'); + var closedSquareBracketPos = host.LastIndexOf(']'); + if (colonPos >= 0 && closedSquareBracketPos < colonPos) + { + host = host.Substring(0, colonPos); + } + + // var baseUri = $"{(IsSecureConnection ? "https" : "http")}://{host}:{LocalEndPoint.Port}"; + var baseUri = $"http://{host}:{LocalEndPoint.Port}"; + + if (!Uri.TryCreate(baseUri + path, UriKind.Absolute, out var url)) + { + _connection.SetError(WebUtility.HtmlEncode($"Invalid url: {baseUri}{path}")); + return; + } + + Url = url; + InitializeQueryString(Url.Query); + + if (ContentLength64 == 0 && (HttpVerb == HttpVerbs.Post || HttpVerb == HttpVerbs.Put)) + { + return; + } + + if (string.Compare(Headers["Expect"], "100-continue", StringComparison.OrdinalIgnoreCase) == 0) + { + _connection.GetResponseStream().InternalWrite(HttpStatus100, 0, HttpStatus100.Length); + } + } + + internal void AddHeader(string header) + { + var colon = header.IndexOf(':'); + if (colon == -1 || colon == 0) + { + _connection.SetError("Bad Request"); + return; + } + + var name = header.Substring(0, colon).Trim(); + var val = header.Substring(colon + 1).Trim(); + + Headers.Set(name, val); + + switch (name.ToLowerInvariant()) + { + case "accept-language": + UserLanguages = val.SplitByComma(); // yes, only split with a ',' + break; + case "accept": + AcceptTypes = val.SplitByComma(); // yes, only split with a ',' + break; + case "content-length": + Headers[HttpHeaderNames.ContentLength] = val.Trim(); + + if (ContentLength64 < 0) + { + _connection.SetError("Invalid Content-Length."); + } + + break; + case "referer": + try + { + UrlReferrer = new Uri(val); + } + catch + { + UrlReferrer = null; + } + + break; + case "cookie": + ParseCookies(val); + + break; + } + } + + // returns true is the stream could be reused. + internal bool FlushInput() + { + if (!HasEntityBody) + { + return true; + } + + var length = 2048; + if (ContentLength64 > 0) + { + length = (int)Math.Min(ContentLength64, length); + } + + var bytes = new byte[length]; + + while (true) + { + try + { + if (InputStream.Read(bytes, 0, length) <= 0) + { + return true; + } + } + catch (ObjectDisposedException) + { + _inputStream = null; + return true; + } + catch + { + return false; + } + } + } + + // Optimized for the following list of methods: + // "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT" + // ***NOTE***: The verb parameter is NOT VALID upon exit if false is returned. + private static bool IsKnownHttpMethod(string method, out HttpVerbs verb) + { + switch (method.Length) + { + case 3: + switch (method[0]) + { + case 'G': + verb = HttpVerbs.Get; + return method[1] == 'E' && method[2] == 'T'; + + case 'P': + verb = HttpVerbs.Put; + return method[1] == 'U' && method[2] == 'T'; + + default: + verb = HttpVerbs.Any; + return false; + } + + case 4: + switch (method[0]) + { + case 'H': + verb = HttpVerbs.Head; + return method[1] == 'E' && method[2] == 'A' && method[3] == 'D'; + + case 'P': + verb = HttpVerbs.Post; + return method[1] == 'O' && method[2] == 'S' && method[3] == 'T'; + + default: + verb = HttpVerbs.Any; + return false; + } + + case 5: + verb = HttpVerbs.Patch; + return method[0] == 'P' + && method[1] == 'A' + && method[2] == 'T' + && method[3] == 'C' + && method[4] == 'H'; + + case 6: + verb = HttpVerbs.Delete; + return method[0] == 'D' + && method[1] == 'E' + && method[2] == 'L' + && method[3] == 'E' + && method[4] == 'T' + && method[5] == 'E'; + + case 7: + verb = HttpVerbs.Options; + return method[0] == 'O' + && method[1] == 'P' + && method[2] == 'T' + && method[3] == 'I' + && method[4] == 'O' + && method[5] == 'N' + && method[6] == 'S'; + + default: + verb = HttpVerbs.Any; + return false; + } + } + + private void ParseCookies(string val) + { + _cookies ??= new CookieList(); + + var cookieStrings = val.SplitByAny(';', ',') + .Where(x => !string.IsNullOrEmpty(x)); + Cookie? current = null; + var version = 0; + + foreach (var str in cookieStrings) + { + if (str.StartsWith("$Version", StringComparison.Ordinal)) + { + version = int.Parse(str.Substring(str.IndexOf('=') + 1).Unquote(), CultureInfo.InvariantCulture); + } + else if (str.StartsWith("$Path", StringComparison.Ordinal) && current != null) + { + current.Path = str.Substring(str.IndexOf('=') + 1).Trim(); + } + else if (str.StartsWith("$Domain", StringComparison.Ordinal) && current != null) + { + current.Domain = str.Substring(str.IndexOf('=') + 1).Trim(); + } + else if (str.StartsWith("$Port", StringComparison.Ordinal) && current != null) + { + current.Port = str.Substring(str.IndexOf('=') + 1).Trim(); + } + else + { + if (current != null) + { + _cookies.Add(current); + } + + current = new Cookie(); + var idx = str.IndexOf('='); + if (idx > 0) + { + current.Name = str.Substring(0, idx).Trim(); + current.Value = str.Substring(idx + 1).Trim(); + } + else + { + current.Name = str.Trim(); + current.Value = string.Empty; + } + + current.Version = version; + } + } + + if (current != null) + { + _cookies.Add(current); + } + } + + private void InitializeQueryString(string query) + { + if (string.IsNullOrEmpty(query)) + { + return; + } + + if (query[0] == '?') + { + query = query.Substring(1); + } + + var components = query.Split('&'); + + foreach (var kv in components) + { + var pos = kv.IndexOf('='); + if (pos == -1) + { + QueryString.Add(null, WebUtility.UrlDecode(kv)); + } + else + { + var key = WebUtility.UrlDecode(kv.Substring(0, pos)); + var val = WebUtility.UrlDecode(kv.Substring(pos + 1)); + + QueryString.Add(key, val); + } + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/HttpListenerResponse.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpListenerResponse.cs new file mode 100644 index 0000000..be8703a --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpListenerResponse.cs @@ -0,0 +1,411 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using EmbedIO.Utilities; + +namespace EmbedIO.Net.Internal +{ + /// + /// Represents an HTTP Listener's response. + /// + /// + internal sealed class HttpListenerResponse : IHttpResponse, IDisposable + { + private readonly HttpConnection _connection; + private readonly HttpListenerRequest _request; + private readonly string _id; + private bool _disposed; + private string _contentType = MimeType.Html; // Same default value as Microsoft's implementation + private CookieList? _cookies; + private bool _keepAlive; + private ResponseStream? _outputStream; + private int _statusCode = 200; + private bool _chunked; + + internal HttpListenerResponse(HttpListenerContext context) + { + _request = context.HttpListenerRequest; + _connection = context.Connection; + _id = context.Id; + _keepAlive = context.Request.KeepAlive; + } + + /// + public Encoding? ContentEncoding { get; set; } = WebServer.DefaultEncoding; + + /// + /// This instance has been disposed. + /// This property is being set and headers were already sent. + public long ContentLength64 + { + get => Headers.ContainsKey(HttpHeaderNames.ContentLength) && long.TryParse(Headers[HttpHeaderNames.ContentLength], out var val) ? val : 0; + + set + { + EnsureCanChangeHeaders(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "Must be >= 0"); + } + + Headers[HttpHeaderNames.ContentLength] = value.ToString(CultureInfo.InvariantCulture); + } + } + + /// + /// This instance has been disposed. + /// This property is being set and headers were already sent. + /// This property is being set to . + /// This property is being set to the empty string. + public string ContentType + { + get => _contentType; + + set + { + EnsureCanChangeHeaders(); + _contentType = Validate.NotNullOrEmpty(nameof(value), value); + } + } + + /// + public ICookieCollection Cookies => CookieCollection; + + /// + public WebHeaderCollection Headers { get; } = new WebHeaderCollection(); + + /// + public bool KeepAlive + { + get => _keepAlive; + + set + { + EnsureCanChangeHeaders(); + _keepAlive = value; + } + } + + /// + public Stream OutputStream => _outputStream ??= _connection.GetResponseStream(); + + /// + public Version ProtocolVersion => _request.ProtocolVersion; + + /// + /// This instance has been disposed. + /// This property is being set and headers were already sent. + public bool SendChunked + { + get => _chunked; + + set + { + EnsureCanChangeHeaders(); + _chunked = value; + } + } + + /// + /// This instance has been disposed. + /// This property is being set and headers were already sent. + public int StatusCode + { + get => _statusCode; + + set + { + EnsureCanChangeHeaders(); + if (value < 100 || value > 999) + { + throw new ArgumentOutOfRangeException(nameof(StatusCode), "StatusCode must be between 100 and 999."); + } + + _statusCode = value; + StatusDescription = HttpListenerResponseHelper.GetStatusDescription(value); + } + } + + /// + public string StatusDescription { get; set; } = "OK"; + + internal CookieList CookieCollection + { + get => _cookies ??= new CookieList(); + set => _cookies = value; + } + + internal bool HeadersSent { get; set; } + + void IDisposable.Dispose() => Close(true); + + public void Close() + { + if (!_disposed) + { + Close(false); + } + } + + /// + public void SetCookie(Cookie cookie) + { + if (cookie == null) + { + throw new ArgumentNullException(nameof(cookie)); + } + + if (_cookies != null) + { + if (_cookies.Any(c => cookie.Name == c.Name && cookie.Domain == c.Domain && cookie.Path == c.Path)) + { + throw new ArgumentException("The cookie already exists."); + } + } + else + { + _cookies = new CookieList(); + } + + _cookies.Add(cookie); + } + + internal MemoryStream SendHeaders(bool closing) + { + if (_contentType != null) + { + var contentTypeValue = _contentType.IndexOf("charset=", StringComparison.Ordinal) == -1 && ContentEncoding is not null + ? $"{_contentType}; charset={ContentEncoding.WebName}" + : _contentType; + + Headers.Add(HttpHeaderNames.ContentType, contentTypeValue); + } + + if (Headers[HttpHeaderNames.Server] == null) + { + Headers.Add(HttpHeaderNames.Server, WebServer.Signature); + } + + if (Headers[HttpHeaderNames.Date] == null) + { + Headers.Add(HttpHeaderNames.Date, HttpDate.Format(DateTime.UtcNow)); + } + + // HTTP did not support chunked transfer encoding before version 1.1; + // besides, there's no point in setting transfer encoding at all without a request body. + if (closing || ProtocolVersion < HttpVersion.Version11) + { + _chunked = false; + } + + // Was content length set to a valid value, AND chunked encoding not set? + // Note that this does not mean that a response body _will_ be sent + // as this could be the response to a HEAD request. + var haveContentLength = !_chunked + && Headers.ContainsKey(HttpHeaderNames.ContentLength) + && long.TryParse(Headers[HttpHeaderNames.ContentLength], NumberStyles.None, CultureInfo.InvariantCulture, out var contentLength) + && contentLength >= 0L; + + if (!haveContentLength) + { + // Content length could have been set to an invalid value (e.g. "-1") + // so we must either force it to 0, or remove the header completely. + if (closing) + { + // Content length was not explicitly set to a valid value, + // and there is no request body. + Headers[HttpHeaderNames.ContentLength] = "0"; + } + else + { + // Content length was not explicitly set to a valid value, + // and we're going to send a request body. + // - Remove possibly invalid Content-Length header + // - Enable chunked transfer encoding for HTTP 1.1 + Headers.Remove(HttpHeaderNames.ContentLength); + if (ProtocolVersion >= HttpVersion.Version11) + { + _chunked = true; + } + } + } + + if (_chunked) + { + Headers.Add(HttpHeaderNames.TransferEncoding, "chunked"); + } + + //// Apache forces closing the connection for these status codes: + //// HttpStatusCode.BadRequest 400 + //// HttpStatusCode.RequestTimeout 408 + //// HttpStatusCode.LengthRequired 411 + //// HttpStatusCode.RequestEntityTooLarge 413 + //// HttpStatusCode.RequestUriTooLong 414 + //// HttpStatusCode.InternalServerError 500 + //// HttpStatusCode.ServiceUnavailable 503 + var reuses = _connection.Reuses; + var keepAlive = _statusCode switch { + 400 => false, + 408 => false, + 411 => false, + 413 => false, + 414 => false, + 500 => false, + 503 => false, + _ => _keepAlive && reuses < 100 + }; + + _keepAlive = keepAlive; + if (keepAlive) + { + Headers.Add(HttpHeaderNames.Connection, "keep-alive"); + if (ProtocolVersion >= HttpVersion.Version11) + { + Headers.Add(HttpHeaderNames.KeepAlive, $"timeout=15,max={100 - reuses}"); + } + } + else + { + Headers.Add(HttpHeaderNames.Connection, "close"); + } + + return WriteHeaders(); + } + + private static void AppendSetCookieHeader(StringBuilder sb, Cookie cookie) + { + if (cookie.Name.Length == 0) + { + return; + } + + _ = sb.Append("Set-Cookie: "); + + if (cookie.Version > 0) + { + _ = sb.Append("Version=").Append(cookie.Version).Append("; "); + } + + _ = sb + .Append(cookie.Name) + .Append('=') + .Append(cookie.Value); + + if (cookie.Expires != DateTime.MinValue) + { + _ = sb + .Append("; Expires=") + .Append(HttpDate.Format(cookie.Expires)); + } + + if (!string.IsNullOrEmpty(cookie.Path)) + { + _ = sb.Append("; Path=").Append(QuotedString(cookie, cookie.Path)); + } + + if (!string.IsNullOrEmpty(cookie.Domain)) + { + _ = sb.Append("; Domain=").Append(QuotedString(cookie, cookie.Domain)); + } + + if (!string.IsNullOrEmpty(cookie.Port)) + { + _ = sb.Append("; Port=").Append(cookie.Port); + } + + if (cookie.Secure) + { + _ = sb.Append("; Secure"); + } + + if (cookie.HttpOnly) + { + _ = sb.Append("; HttpOnly"); + } + + _ = sb.Append("\r\n"); + } + + private static string QuotedString(Cookie cookie, string value) + => cookie.Version == 0 || value.IsToken() ? value : "\"" + value.Replace("\"", "\\\"") + "\""; + + private void Close(bool force) + { + _disposed = true; + + _connection.Close(force); + } + + private string GetHeaderData() + { + var sb = new StringBuilder() + .Append("HTTP/") + .Append(ProtocolVersion) + .Append(' ') + .Append(_statusCode) + .Append(' ') + .Append(StatusDescription) + .Append("\r\n"); + + foreach (var key in Headers.AllKeys.Where(x => x != "Set-Cookie")) + { + _ = sb + .Append(key) + .Append(": ") + .Append(Headers[key]) + .Append("\r\n"); + } + + if (_cookies != null) + { + foreach (var cookie in _cookies) + { + AppendSetCookieHeader(sb, cookie); + } + } + + if (Headers.ContainsKey(HttpHeaderNames.SetCookie)) + { + foreach (var cookie in CookieList.Parse(Headers[HttpHeaderNames.SetCookie])) + { + AppendSetCookieHeader(sb, cookie); + } + } + + return sb.Append("\r\n").ToString(); + } + + private MemoryStream WriteHeaders() + { + var stream = new MemoryStream(); + var data = WebServer.DefaultEncoding.GetBytes(GetHeaderData()); + var preamble = WebServer.DefaultEncoding.GetPreamble(); + stream.Write(preamble, 0, preamble.Length); + stream.Write(data, 0, data.Length); + + _outputStream ??= _connection.GetResponseStream(); + + // Assumes that the ms was at position 0 + stream.Position = preamble.Length; + HeadersSent = true; + + return stream; + } + + private void EnsureCanChangeHeaders() + { + if (_disposed) + { + throw new ObjectDisposedException(_id); + } + + if (HeadersSent) + { + throw new InvalidOperationException("Header values cannot be changed after headers are sent."); + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/HttpListenerResponseHelper.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpListenerResponseHelper.cs new file mode 100644 index 0000000..944a1b6 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/HttpListenerResponseHelper.cs @@ -0,0 +1,55 @@ +namespace EmbedIO.Net.Internal +{ + internal static class HttpListenerResponseHelper + { + internal static string GetStatusDescription(int code) => code switch { + 100 => "Continue", + 101 => "Switching Protocols", + 102 => "Processing", + 200 => "OK", + 201 => "Created", + 202 => "Accepted", + 203 => "Non-Authoritative Information", + 204 => "No Content", + 205 => "Reset Content", + 206 => "Partial Content", + 207 => "Multi-Status", + 300 => "Multiple Choices", + 301 => "Moved Permanently", + 302 => "Found", + 303 => "See Other", + 304 => "Not Modified", + 305 => "Use Proxy", + 307 => "Temporary 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", + 422 => "Unprocessable Entity", + 423 => "Locked", + 424 => "Failed Dependency", + 500 => "Internal Server Error", + 501 => "Not Implemented", + 502 => "Bad Gateway", + 503 => "Service Unavailable", + 504 => "Gateway Timeout", + 505 => "Http Version Not Supported", + 507 => "Insufficient Storage", + _ => string.Empty + }; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/ListenerPrefix.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/ListenerPrefix.cs new file mode 100644 index 0000000..7bd2f1e --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/ListenerPrefix.cs @@ -0,0 +1,35 @@ +using System; + +namespace EmbedIO.Net.Internal +{ + internal sealed class ListenerPrefix + { + public ListenerPrefix(string uri) + { + var parsedUri = ListenerUri.Parse(uri); + Secure = parsedUri.Secure; + Host = parsedUri.Host; + Port = parsedUri.Port; + Path = parsedUri.Path; + } + + public HttpListener? Listener { get; set; } + + public bool Secure { get; } + + public string Host { get; } + + public int Port { get; } + + public string Path { get; } + + public static void CheckUri(string uri) + { + _ = ListenerUri.Parse(uri); + } + + public bool IsValid() => Path.IndexOf('%') == -1 && Path.IndexOf("//", StringComparison.Ordinal) == -1; + + public override string ToString() => $"{Host}:{Port} ({(Secure ? "Secure" : "Insecure")}"; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/ListenerUri.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/ListenerUri.cs new file mode 100644 index 0000000..e89080d --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/ListenerUri.cs @@ -0,0 +1,91 @@ +using System; + +namespace EmbedIO.Net.Internal +{ + internal class ListenerUri + { + private ListenerUri(bool secure, + string host, + int port, + string path) + { + Secure = secure; + Host = host; + Port = port; + Path = path; + } + + public bool Secure { get; private set; } + + public string Host { get; private set; } + + public int Port { get; private set; } + + public string Path { get; private set; } + + public static ListenerUri Parse(string uri) + { + bool secure; + int port; + int parsingPosition; + if (uri.StartsWith("http://")) + { + secure = false; + port = 80; + parsingPosition = "http://".Length; + } + else if (uri.StartsWith("https://")) + { + secure = true; + port = 443; + parsingPosition = "https://".Length; + } + else + { + throw new Exception("Only 'http' and 'https' schemes are supported."); + } + + var startOfPath = uri.IndexOf('/', parsingPosition); + if (startOfPath == -1) + { + throw new ArgumentException("Path should end in '/'."); + } + + var hostWithPort = uri.Substring(parsingPosition, startOfPath - parsingPosition); + + var startOfPortWithColon = hostWithPort.LastIndexOf(':'); + if (startOfPortWithColon > -1) + { + startOfPortWithColon += parsingPosition; + } + + var endOfIpV6 = hostWithPort.LastIndexOf(']'); + if (endOfIpV6 > -1) + { + endOfIpV6 += parsingPosition; + } + + if (endOfIpV6 > startOfPortWithColon) + { + startOfPortWithColon = -1; + } + + if (startOfPortWithColon != -1 && startOfPortWithColon < startOfPath) + { + if (!int.TryParse(uri.Substring(startOfPortWithColon + 1, startOfPath - startOfPortWithColon - 1), out port) || port <= 0 || port >= 65535) + { + throw new ArgumentException("Invalid port."); + } + } + + var host = uri.Substring(parsingPosition, (startOfPortWithColon == -1 ? startOfPath : startOfPortWithColon) - parsingPosition); + var path = uri.Substring(startOfPath); + if (!path.EndsWith("/")) + { + throw new ArgumentException("Path should end in '/'."); + } + + return new ListenerUri(secure, host, port, path); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/NetExtensions.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/NetExtensions.cs new file mode 100644 index 0000000..4d80bd0 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/NetExtensions.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using Swan; + +namespace EmbedIO.Net.Internal +{ + /// + /// Represents some System.NET custom extensions. + /// + internal static class NetExtensions + { + internal static byte[] ToByteArray(this ushort value, Endianness order) + { + var bytes = BitConverter.GetBytes(value); + if (!order.IsHostOrder()) + { + Array.Reverse(bytes); + } + + return bytes; + } + + internal static byte[] ToByteArray(this ulong value, Endianness order) + { + var bytes = BitConverter.GetBytes(value); + if (!order.IsHostOrder()) + { + Array.Reverse(bytes); + } + + return bytes; + } + + internal static byte[] ToHostOrder(this byte[] source, Endianness sourceOrder) + => source.Length < 1 ? source + : sourceOrder.IsHostOrder() ? source + : source.Reverse().ToArray(); + + // true: !(true ^ true) or !(false ^ false) + // false: !(true ^ false) or !(false ^ true) + private static bool IsHostOrder(this Endianness order) + => !(BitConverter.IsLittleEndian ^ (order == Endianness.Little)); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/RequestStream.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/RequestStream.cs new file mode 100644 index 0000000..9e7fbd6 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/RequestStream.cs @@ -0,0 +1,143 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace EmbedIO.Net.Internal +{ + internal class RequestStream : Stream + { + private readonly Stream _stream; + private readonly byte[] _buffer; + private int _offset; + private int _length; + private long _remainingBody; + + internal RequestStream(Stream stream, byte[] buffer, int offset, int length, long contentLength = -1) + { + _stream = stream; + _buffer = buffer; + _offset = offset; + _length = length; + _remainingBody = contentLength; + } + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() + { + } + + public override int Read([In, Out] byte[] buffer, int offset, int count) + { + // Call FillFromBuffer to check for buffer boundaries even when remaining_body is 0 + var nread = FillFromBuffer(buffer, offset, count); + + if (nread == -1) + { + // No more bytes available (Content-Length) + return 0; + } + + if (nread > 0) + { + return nread; + } + + nread = _stream.Read(buffer, offset, count); + + if (nread > 0 && _remainingBody > 0) + { + _remainingBody -= nread; + } + + return nread; + } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + // Returns 0 if we can keep reading from the base stream, + // > 0 if we read something from the buffer. + // -1 if we had a content length set and we finished reading that many bytes. + private int FillFromBuffer(byte[] buffer, int off, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (off < 0) + { + throw new ArgumentOutOfRangeException(nameof(off), "< 0"); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "< 0"); + } + + var len = buffer.Length; + + if (off > len) + { + throw new ArgumentException("destination offset is beyond array size"); + } + + if (off > len - count) + { + throw new ArgumentException("Reading would overrun buffer"); + } + + if (_remainingBody == 0) + { + return -1; + } + + if (_length == 0) + { + return 0; + } + + var size = Math.Min(_length, count); + if (_remainingBody > 0) + { + size = (int) Math.Min(size, _remainingBody); + } + + if (_offset > _buffer.Length - size) + { + size = Math.Min(size, _buffer.Length - _offset); + } + + if (size == 0) + { + return 0; + } + + Buffer.BlockCopy(_buffer, _offset, buffer, off, size); + _offset += size; + _length -= size; + if (_remainingBody > 0) + { + _remainingBody -= size; + } + + return size; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/ResponseStream.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/ResponseStream.cs new file mode 100644 index 0000000..884d305 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/ResponseStream.cs @@ -0,0 +1,190 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace EmbedIO.Net.Internal +{ + internal class ResponseStream : Stream + { + private static readonly byte[] CrLf = { 13, 10 }; + private readonly object _headersSyncRoot = new (); + + private readonly Stream _stream; + private readonly HttpListenerResponse _response; + private readonly bool _ignoreErrors; + private bool _disposed; + private bool _trailerSent; + + internal ResponseStream(Stream stream, HttpListenerResponse response, bool ignoreErrors) + { + _response = response; + _ignoreErrors = ignoreErrors; + _stream = stream; + } + + /// + public override bool CanRead => false; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => true; + + /// + public override long Length => throw new NotSupportedException(); + + /// + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + /// + public override void Flush() + { + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(ResponseStream)); + } + + byte[] bytes; + var ms = GetHeaders(false); + var chunked = _response.SendChunked; + + if (ms != null) + { + var start = ms.Position; // After the possible preamble for the encoding + ms.Position = ms.Length; + if (chunked) + { + bytes = GetChunkSizeBytes(count, false); + ms.Write(bytes, 0, bytes.Length); + } + + var newCount = Math.Min(count, 16384 - (int)ms.Position + (int)start); + ms.Write(buffer, offset, newCount); + count -= newCount; + offset += newCount; + InternalWrite(ms.ToArray(), (int)start, (int)(ms.Length - start)); + ms.SetLength(0); + ms.Capacity = 0; // 'dispose' the buffer in ms. + } + else if (chunked) + { + bytes = GetChunkSizeBytes(count, false); + InternalWrite(bytes, 0, bytes.Length); + } + + if (count > 0) + { + InternalWrite(buffer, offset, count); + } + + if (chunked) + { + InternalWrite(CrLf, 0, 2); + } + } + + /// + public override int Read([In, Out] byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + /// + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + /// + public override void SetLength(long value) => throw new NotSupportedException(); + + internal void InternalWrite(byte[] buffer, int offset, int count) + { + if (_ignoreErrors) + { + try + { + _stream.Write(buffer, offset, count); + } + catch + { + // ignored + } + } + else + { + _stream.Write(buffer, offset, count); + } + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + _disposed = true; + + if (!disposing) + { + return; + } + + using var ms = GetHeaders(true); + var chunked = _response.SendChunked; + + if (_stream.CanWrite) + { + try + { + byte[] bytes; + if (ms != null) + { + var start = ms.Position; + if (chunked && !_trailerSent) + { + bytes = GetChunkSizeBytes(0, true); + ms.Position = ms.Length; + ms.Write(bytes, 0, bytes.Length); + } + + InternalWrite(ms.ToArray(), (int)start, (int)(ms.Length - start)); + _trailerSent = true; + } + else if (chunked && !_trailerSent) + { + bytes = GetChunkSizeBytes(0, true); + InternalWrite(bytes, 0, bytes.Length); + _trailerSent = true; + } + } + catch (ObjectDisposedException) + { + // Ignored + } + catch (IOException) + { + // Ignore error due to connection reset by peer + } + } + + _response.Close(); + } + + private static byte[] GetChunkSizeBytes(int size, bool final) => WebServer.DefaultEncoding.GetBytes($"{size:x}\r\n{(final ? "\r\n" : string.Empty)}"); + + private MemoryStream? GetHeaders(bool closing) + { + lock (_headersSyncRoot) + { + return _response.HeadersSent ? null : _response.SendHeaders(closing); + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/StringExtensions.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/StringExtensions.cs new file mode 100644 index 0000000..fab8333 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/StringExtensions.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EmbedIO.Net.Internal +{ + internal static class StringExtensions + { + private const string TokenSpecialChars = "()<>@,;:\\\"/[]?={} \t"; + + internal static bool IsToken(this string @this) + => @this.All(c => c >= 0x20 && c < 0x7f && TokenSpecialChars.IndexOf(c) < 0); + + internal static IEnumerable SplitHeaderValue(this string @this, bool useCookieSeparators) + { + var len = @this.Length; + + var buff = new StringBuilder(32); + var escaped = false; + var quoted = false; + + for (var i = 0; i < len; i++) + { + var c = @this[i]; + + if (c == '"') + { + if (escaped) + { + escaped = false; + } + else + { + quoted = !quoted; + } + } + else if (c == '\\') + { + if (i < len - 1 && @this[i + 1] == '"') + { + escaped = true; + } + } + else if (c == ',' || (useCookieSeparators && c == ';')) + { + if (!quoted) + { + yield return buff.ToString(); + buff.Length = 0; + + continue; + } + } + + _ = buff.Append(c); + } + + if (buff.Length > 0) + { + yield return buff.ToString(); + } + } + + internal static string Unquote(this string str) + { + var start = str.IndexOf('\"'); + var end = str.LastIndexOf('\"'); + + if (start >= 0 && end >= 0) + { + str = str.Substring(start + 1, end - 1); + } + + return str.Trim(); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/SystemCookieCollection.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/SystemCookieCollection.cs new file mode 100644 index 0000000..b792aef --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/SystemCookieCollection.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net; + +namespace EmbedIO.Net.Internal +{ + /// + /// Represents a wrapper for System.Net.CookieCollection. + /// + /// + internal sealed class SystemCookieCollection : ICookieCollection + { + private readonly CookieCollection _collection; + + /// + /// Initializes a new instance of the class. + /// + /// The cookie collection. + public SystemCookieCollection(CookieCollection collection) + { + _collection = collection; + } + + /// + public int Count => _collection.Count; + + /// + public bool IsSynchronized => _collection.IsSynchronized; + + /// + public object SyncRoot => _collection.SyncRoot; + + /// + public Cookie? this[string name] => _collection[name]; + + /// + IEnumerator IEnumerable.GetEnumerator() => _collection.OfType().GetEnumerator(); + + /// + public IEnumerator GetEnumerator() => _collection.GetEnumerator(); + + /// + public void CopyTo(Array array, int index) => _collection.CopyTo(array, index); + + /// + public void CopyTo(Cookie[] array, int index) => _collection.CopyTo(array, index); + + /// + public void Add(Cookie cookie) => _collection.Add(cookie); + + /// + public bool Contains(Cookie cookie) => _collection.OfType().Contains(cookie); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/SystemHttpContext.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/SystemHttpContext.cs new file mode 100644 index 0000000..4ebd097 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/SystemHttpContext.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Authentication; +using EmbedIO.Internal; +using EmbedIO.Routing; +using EmbedIO.Sessions; +using EmbedIO.Utilities; +using EmbedIO.WebSockets; +using EmbedIO.WebSockets.Internal; +using Swan.Logging; + +namespace EmbedIO.Net.Internal +{ + internal sealed class SystemHttpContext : IHttpContextImpl + { + private readonly System.Net.HttpListenerContext _context; + + private readonly TimeKeeper _ageKeeper = new (); + + private readonly Stack> _closeCallbacks = new (); + + private bool _closed; + + public SystemHttpContext(System.Net.HttpListenerContext context) + { + _context = context; + + Request = new SystemHttpRequest(_context); + User = _context.User ?? Auth.NoUser; + Response = new SystemHttpResponse(_context); + Id = UniqueIdGenerator.GetNext(); + LocalEndPoint = Request.LocalEndPoint; + RemoteEndPoint = Request.RemoteEndPoint; + Route = RouteMatch.None; + Session = SessionProxy.None; + } + + public string Id { get; } + + public CancellationToken CancellationToken { get; set; } + + public long Age => _ageKeeper.ElapsedTime; + + public IPEndPoint LocalEndPoint { get; } + + public IPEndPoint RemoteEndPoint { get; } + + public IHttpRequest Request { get; } + + public RouteMatch Route { get; set; } + + public string RequestedPath => Route.SubPath ?? string.Empty; // It will never be empty, because modules are matched via base routes - this is just to silence a warning. + + public IHttpResponse Response { get; } + + public IPrincipal User { get; set; } + + public ISessionProxy Session { get; set; } + + public bool SupportCompressedRequests { get; set; } + + public IDictionary Items { get; } = new Dictionary(); + + public bool IsHandled { get; private set; } + + public MimeTypeProviderStack MimeTypeProviders { get; } = new MimeTypeProviderStack(); + + public void SetHandled() => IsHandled = true; + + public void OnClose(Action callback) + { + if (_closed) + { + throw new InvalidOperationException("HTTP context has already been closed."); + } + + _closeCallbacks.Push(Validate.NotNull(nameof(callback), callback)); + } + + public async Task AcceptWebSocketAsync( + IEnumerable requestedProtocols, + string acceptedProtocol, + int receiveBufferSize, + TimeSpan keepAliveInterval, + CancellationToken cancellationToken) + { + var context = await _context.AcceptWebSocketAsync( + acceptedProtocol.NullIfEmpty(), // Empty string would throw; use null to signify "no subprotocol" here. + receiveBufferSize, + keepAliveInterval) + .ConfigureAwait(false); + return new WebSocketContext(this, context.SecWebSocketVersion, requestedProtocols, acceptedProtocol, new SystemWebSocket(context.WebSocket), cancellationToken); + } + + public void Close() + { + _closed = true; + + // Always close the response stream no matter what. + Response.Close(); + + foreach (var callback in _closeCallbacks) + { + try + { + callback(this); + } + catch (Exception e) + { + e.Log("HTTP context", "[Id] Exception thrown by a HTTP context close callback."); + } + } + } + + public string GetMimeType(string extension) + => MimeTypeProviders.GetMimeType(extension); + + public bool TryDetermineCompression(string mimeType, out bool preferCompression) + => MimeTypeProviders.TryDetermineCompression(mimeType, out preferCompression); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/SystemHttpListener.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/SystemHttpListener.cs new file mode 100644 index 0000000..5d14883 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/SystemHttpListener.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace EmbedIO.Net.Internal +{ + /// + /// Represents a wrapper for Microsoft HTTP Listener. + /// + internal class SystemHttpListener : IHttpListener + { + private readonly System.Net.HttpListener _httpListener; + + public SystemHttpListener(System.Net.HttpListener httpListener) + { + _httpListener = httpListener; + } + + /// + public bool IgnoreWriteExceptions + { + get => _httpListener.IgnoreWriteExceptions; + set => _httpListener.IgnoreWriteExceptions = value; + } + + /// + public List Prefixes => _httpListener.Prefixes.ToList(); + + /// + public bool IsListening => _httpListener.IsListening; + + /// + public string Name { get; } = "Microsoft HTTP Listener"; + + /// + public void Start() => _httpListener.Start(); + + /// + public void Stop() => _httpListener.Stop(); + + /// + public void AddPrefix(string urlPrefix) => _httpListener.Prefixes.Add(urlPrefix); + + /// + public async Task GetContextAsync(CancellationToken cancellationToken) + { + // System.Net.HttpListener.GetContextAsync may throw ObjectDisposedException + // when stopping a WebServer. This has been observed on Mono 5.20.1.19 + // on Raspberry Pi, but the fact remains that the method does not take + // a CancellationToken as parameter, and WebServerBase<>.RunAsync counts on it. + System.Net.HttpListenerContext context; + try + { + context = await _httpListener.GetContextAsync().ConfigureAwait(false); + } + catch (Exception e) when (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException( + "Probable cancellation detected by catching an exception in System.Net.HttpListener.GetContextAsync", + e, + cancellationToken); + } + + return new SystemHttpContext(context); + } + + void IDisposable.Dispose() => ((IDisposable)_httpListener)?.Dispose(); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/SystemHttpRequest.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/SystemHttpRequest.cs new file mode 100644 index 0000000..c266a35 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/SystemHttpRequest.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Net; +using System.Text; + +namespace EmbedIO.Net.Internal +{ + /// + /// Represents a wrapper for HttpListenerContext.Request. + /// + /// + public class SystemHttpRequest : IHttpRequest + { + private readonly System.Net.HttpListenerRequest _request; + + /// + /// Initializes a new instance of the class. + /// + /// The context. + public SystemHttpRequest(System.Net.HttpListenerContext context) + { + _request = context.Request; + _ = Enum.TryParse(_request.HttpMethod.Trim(), true, out var verb); + HttpVerb = verb; + Cookies = new SystemCookieCollection(_request.Cookies); + LocalEndPoint = _request.LocalEndPoint!; + RemoteEndPoint = _request.RemoteEndPoint!; + } + + /// + public NameValueCollection Headers => _request.Headers; + + /// + public Version ProtocolVersion => _request.ProtocolVersion; + + /// + public bool KeepAlive => _request.KeepAlive; + + /// + public ICookieCollection Cookies { get; } + + /// + public string RawUrl => _request.RawUrl; + + /// + public NameValueCollection QueryString => _request.QueryString; + + /// + public string HttpMethod => _request.HttpMethod; + + /// + public HttpVerbs HttpVerb { get; } + + /// + public Uri Url => _request.Url; + + /// + public bool HasEntityBody => _request.HasEntityBody; + + /// + public Stream InputStream => _request.InputStream; + + /// + public Encoding ContentEncoding + { + get + { + if (!_request.HasEntityBody || _request.ContentType == null) + { + return WebServer.DefaultEncoding; + } + + var charSet = HeaderUtility.GetCharset(ContentType); + if (string.IsNullOrEmpty(charSet)) + { + return WebServer.DefaultEncoding; + } + + try + { + return Encoding.GetEncoding(charSet); + } + catch (ArgumentException) + { + return WebServer.DefaultEncoding; + } + } + } + + /// + public IPEndPoint RemoteEndPoint { get; } + + /// + public bool IsSecureConnection => _request.IsSecureConnection; + + /// + public bool IsLocal => _request.IsLocal; + + /// + public string UserAgent => _request.UserAgent; + + /// + public bool IsWebSocketRequest => _request.IsWebSocketRequest; + + /// + public IPEndPoint LocalEndPoint { get; } + + /// + public string ContentType => _request.ContentType; + + /// + public long ContentLength64 => _request.ContentLength64; + + /// + public bool IsAuthenticated => _request.IsAuthenticated; + + /// + public Uri? UrlReferrer => _request.UrlReferrer; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/SystemHttpResponse.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/SystemHttpResponse.cs new file mode 100644 index 0000000..a3496f6 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/SystemHttpResponse.cs @@ -0,0 +1,97 @@ +using System; +using System.IO; +using System.Net; +using System.Text; + +namespace EmbedIO.Net.Internal +{ + /// + /// Represents a wrapper for HttpListenerContext.Response. + /// + /// + public class SystemHttpResponse : IHttpResponse + { + private readonly System.Net.HttpListenerResponse _response; + + /// + /// Initializes a new instance of the class. + /// + /// The context. + public SystemHttpResponse(System.Net.HttpListenerContext context) + { + _response = context.Response; + Cookies = new SystemCookieCollection(_response.Cookies); + } + + /// + public WebHeaderCollection Headers => _response.Headers; + + /// + public int StatusCode + { + get => _response.StatusCode; + set => _response.StatusCode = value; + } + + /// + public long ContentLength64 + { + get => _response.ContentLength64; + set => _response.ContentLength64 = value; + } + + /// + public string ContentType + { + get => _response.ContentType; + set => _response.ContentType = value; + } + + /// + public Stream OutputStream => _response.OutputStream; + + /// + public ICookieCollection Cookies { get; } + + /// + public Encoding? ContentEncoding + { + get => _response.ContentEncoding; + set => _response.ContentEncoding = value; + } + + /// + public bool KeepAlive + { + get => _response.KeepAlive; + set => _response.KeepAlive = value; + } + + /// + public bool SendChunked + { + get => _response.SendChunked; + set => _response.SendChunked = value; + } + + /// + public Version ProtocolVersion + { + get => _response.ProtocolVersion; + set => _response.ProtocolVersion = value; + } + + /// + public string StatusDescription + { + get => _response.StatusDescription; + set => _response.StatusDescription = value; + } + + /// + public void SetCookie(Cookie cookie) => _response.SetCookie(cookie); + + /// + public void Close() => _response.OutputStream?.Dispose(); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Net/Internal/WebSocketHandshakeResponse.cs b/Vendor/EmbedIO-3.5.2/Net/Internal/WebSocketHandshakeResponse.cs new file mode 100644 index 0000000..a5dee52 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Net/Internal/WebSocketHandshakeResponse.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Specialized; +using System.Globalization; +using System.Net; +using System.Text; + +namespace EmbedIO.Net.Internal +{ + internal class WebSocketHandshakeResponse + { + private const int HandshakeStatusCode = (int)HttpStatusCode.SwitchingProtocols; + + internal WebSocketHandshakeResponse(IHttpContext context) + { + ProtocolVersion = HttpVersion.Version11; + Headers = context.Response.Headers; + Headers.Clear(); // Use only headers mentioned in RFC6455 - scrap all the rest. + StatusCode = HandshakeStatusCode; + Reason = HttpListenerResponseHelper.GetStatusDescription(HandshakeStatusCode); + + Headers[HttpHeaderNames.Upgrade] = "websocket"; + Headers[HttpHeaderNames.Connection] = "Upgrade"; + + foreach (var cookie in context.Request.Cookies) + { + Headers.Add("Set-Cookie", cookie.ToString()); + } + } + + public string Reason { get; } + + public int StatusCode { get; } + + public NameValueCollection Headers { get; } + + public Version ProtocolVersion { get; } + + public override string ToString() + { + var output = new StringBuilder(64) + .AppendFormat(CultureInfo.InvariantCulture, "HTTP/{0} {1} {2}\r\n", ProtocolVersion, StatusCode, Reason); + + foreach (var key in Headers.AllKeys) + { + _ = output.AppendFormat(CultureInfo.InvariantCulture, "{0}: {1}\r\n", key, Headers[key]); + } + + _ = output.Append("\r\n"); + + return output.ToString(); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/RequestDeserializer.cs b/Vendor/EmbedIO-3.5.2/RequestDeserializer.cs new file mode 100644 index 0000000..dc97545 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/RequestDeserializer.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading.Tasks; +using Swan.Formatters; +using Swan.Logging; + +namespace EmbedIO +{ + /// + /// Provides standard request deserialization callbacks. + /// + public static class RequestDeserializer + { + /// + /// The default request deserializer used by EmbedIO. + /// Equivalent to . + /// + /// The expected type of the deserialized data. + /// The whose request body is to be deserialized. + /// A Task, representing the ongoing operation, + /// whose result will be the deserialized data. + public static Task Default(IHttpContext context) => Json(context); + + /// + /// Asynchronously deserializes a request body in JSON format. + /// + /// The expected type of the deserialized data. + /// The whose request body is to be deserialized. + /// A Task, representing the ongoing operation, + /// whose result will be the deserialized data. + public static Task Json(IHttpContext context) => JsonInternal(context, default); + + /// + /// Returns a RequestDeserializerCallback + /// that will deserialize an HTTP request body in JSON format, using the specified property name casing. + /// + /// The expected type of the deserialized data. + /// The to use. + /// A that can be used to deserialize + /// a JSON request body. + public static RequestDeserializerCallback Json(JsonSerializerCase jsonSerializerCase) + => context => JsonInternal(context, jsonSerializerCase); + + private static async Task JsonInternal(IHttpContext context, JsonSerializerCase jsonSerializerCase) + { + string body; + using (var reader = context.OpenRequestText()) + { + body = await reader.ReadToEndAsync().ConfigureAwait(false); + } + + try + { + return Swan.Formatters.Json.Deserialize(body, jsonSerializerCase); + } + catch (FormatException) + { + $"[{context.Id}] Cannot convert JSON request body to {typeof(TData).Name}, sending 400 Bad Request..." + .Warn($"{nameof(RequestDeserializer)}.{nameof(Json)}"); + + throw HttpException.BadRequest("Incorrect request data format."); + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/RequestDeserializerCallback`1.cs b/Vendor/EmbedIO-3.5.2/RequestDeserializerCallback`1.cs new file mode 100644 index 0000000..74ea808 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/RequestDeserializerCallback`1.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace EmbedIO +{ + /// + /// A callback used to deserialize an HTTP request body. + /// + /// The expected type of the deserialized data. + /// The whose request body is to be deserialized. + /// A Task, representing the ongoing operation, + /// whose result will be the deserialized data. + public delegate Task RequestDeserializerCallback(IHttpContext context); +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/RequestHandler.cs b/Vendor/EmbedIO-3.5.2/RequestHandler.cs new file mode 100644 index 0000000..4258b53 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/RequestHandler.cs @@ -0,0 +1,61 @@ +using System; +using EmbedIO.Internal; + +namespace EmbedIO +{ + /// + /// Provides standard request handler callbacks. + /// + /// + public static class RequestHandler + { + /// + /// Returns an exception object that, when thrown from a module's + /// HandleRequestAsync method, will cause the HTTP context + /// to be passed down along the module chain, regardless of the value of the module's + /// IsFinalHandler property. + /// + /// A newly-created . + public static Exception PassThrough() => new RequestHandlerPassThroughException(); + + /// + /// Returns a that unconditionally sends a 401 Unauthorized response. + /// + /// A message to include in the response. + /// A . + public static RequestHandlerCallback ThrowUnauthorized(string? message = null) + => _ => throw HttpException.Unauthorized(message); + + /// + /// Returns a that unconditionally sends a 403 Forbidden response. + /// + /// A message to include in the response. + /// A . + public static RequestHandlerCallback ThrowForbidden(string? message = null) + => _ => throw HttpException.Forbidden(message); + + /// + /// Returns a that unconditionally sends a 400 Bad Request response. + /// + /// A message to include in the response. + /// A . + public static RequestHandlerCallback ThrowBadRequest(string? message = null) + => _ => throw HttpException.BadRequest(message); + + /// + /// Returns a that unconditionally sends a 404 Not Found response. + /// + /// A message to include in the response. + /// A . + public static RequestHandlerCallback ThrowNotFound(string? message = null) + => _ => throw HttpException.NotFound(message); + + /// + /// Returns a that unconditionally sends a 405 Method Not Allowed response. + /// + /// A message to include in the response. + /// A . + public static RequestHandlerCallback ThrowMethodNotAllowed(string? message = null) + => _ => throw HttpException.MethodNotAllowed(message); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/RequestHandlerCallback.cs b/Vendor/EmbedIO-3.5.2/RequestHandlerCallback.cs new file mode 100644 index 0000000..6e95234 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/RequestHandlerCallback.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace EmbedIO +{ + /// + /// A callback used to handle a request. + /// + /// An interface representing the context of the request. + /// A representing the ongoing operation. + public delegate Task RequestHandlerCallback(IHttpContext context); +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/ResponseSerializer.cs b/Vendor/EmbedIO-3.5.2/ResponseSerializer.cs new file mode 100644 index 0000000..311d4ad --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/ResponseSerializer.cs @@ -0,0 +1,181 @@ +using EmbedIO.Utilities; +using Swan.Formatters; +using System; +using System.Threading.Tasks; + +namespace EmbedIO +{ + /// + /// Provides standard response serializer callbacks. + /// + /// + public static class ResponseSerializer + { + /// + /// The default response serializer callback used by EmbedIO. + /// Equivalent to Json. + /// + public static readonly ResponseSerializerCallback Default = Json; + + private static readonly ResponseSerializerCallback ChunkedEncodingBaseSerializer = GetBaseSerializer(false); + private static readonly ResponseSerializerCallback BufferingBaseSerializer = GetBaseSerializer(true); + + /// + /// Serializes data in JSON format to a HTTP response, + /// using the utility class. + /// + /// The HTTP context of the request. + /// The data to serialize. + /// A representing the ongoing operation. + public static async Task Json(IHttpContext context, object? data) + { + context.Response.ContentType = MimeType.Json; + context.Response.ContentEncoding = WebServer.Utf8NoBomEncoding; + await ChunkedEncodingBaseSerializer(context, Swan.Formatters.Json.Serialize(data)).ConfigureAwait(false); + } + + /// + /// Serializes data in JSON format with the specified + /// to a HTTP response, using the utility class. + /// + /// The JSON serializer case. + /// A that can be used to serialize + /// data to a HTTP response. + public static ResponseSerializerCallback Json(JsonSerializerCase jsonSerializerCase) + => async (context, data) => { + context.Response.ContentType = MimeType.Json; + context.Response.ContentEncoding = WebServer.Utf8NoBomEncoding; + await ChunkedEncodingBaseSerializer(context, Swan.Formatters.Json.Serialize(data, jsonSerializerCase)) + .ConfigureAwait(false); + }; + + /// + /// Serializes data in JSON format with the specified + /// to a HTTP response, using the utility class. + /// + /// The JSON serializer options. + /// A that can be used to serialize + /// data to a HTTP response. + /// + /// is . + /// + public static ResponseSerializerCallback Json(SerializerOptions serializerOptions) + { + _ = Validate.NotNull(nameof(serializerOptions), serializerOptions); + + return async (context, data) => { + context.Response.ContentType = MimeType.Json; + context.Response.ContentEncoding = WebServer.Utf8NoBomEncoding; + await ChunkedEncodingBaseSerializer(context, Swan.Formatters.Json.Serialize(data, serializerOptions)) + .ConfigureAwait(false); + }; + } + + /// + /// Serializes data in JSON format to a HTTP response, using the utility class. + /// + /// to write the response body to a memory buffer first, + /// then send it all together with a Content-Length header; to use chunked + /// transfer encoding. + /// A that can be used to serialize + /// data to a HTTP response. + public static ResponseSerializerCallback Json(bool bufferResponse) + => async (context, data) => { + context.Response.ContentType = MimeType.Json; + context.Response.ContentEncoding = WebServer.Utf8NoBomEncoding; + var baseSerializer = None(bufferResponse); + await baseSerializer(context, Swan.Formatters.Json.Serialize(data)) + .ConfigureAwait(false); + }; + + /// + /// Serializes data in JSON format with the specified + /// to a HTTP response, using the utility class. + /// + /// to write the response body to a memory buffer first, + /// then send it all together with a Content-Length header; to use chunked + /// transfer encoding. + /// The JSON serializer case. + /// A that can be used to serialize + /// data to a HTTP response. + public static ResponseSerializerCallback Json(bool bufferResponse, JsonSerializerCase jsonSerializerCase) + => async (context, data) => { + context.Response.ContentType = MimeType.Json; + context.Response.ContentEncoding = WebServer.Utf8NoBomEncoding; + var baseSerializer = None(bufferResponse); + await baseSerializer(context, Swan.Formatters.Json.Serialize(data, jsonSerializerCase)) + .ConfigureAwait(false); + }; + + /// + /// Serializes data in JSON format with the specified + /// to a HTTP response, using the utility class. + /// + /// to write the response body to a memory buffer first, + /// then send it all together with a Content-Length header; to use chunked + /// transfer encoding. + /// The JSON serializer options. + /// A that can be used to serialize + /// data to a HTTP response. + /// + /// is . + /// + public static ResponseSerializerCallback Json(bool bufferResponse, SerializerOptions serializerOptions) + { + _ = Validate.NotNull(nameof(serializerOptions), serializerOptions); + + return async (context, data) => { + context.Response.ContentType = MimeType.Json; + context.Response.ContentEncoding = WebServer.Utf8NoBomEncoding; + var baseSerializer = None(bufferResponse); + await baseSerializer(context, Swan.Formatters.Json.Serialize(data, serializerOptions)) + .ConfigureAwait(false); + }; + } + + /// + /// Sends data in a HTTP response without serialization. + /// + /// to write the response body to a memory buffer first, + /// then send it all together with a Content-Length header; to use chunked + /// transfer encoding. + /// A that can be used to serialize data to a HTTP response. + /// + /// s and one-dimensional arrays of s + /// are sent to the client unchanged; every other type is converted to a string. + /// The ContentType set on the response is used to negotiate + /// a compression method, according to request headers. + /// Strings (and other types converted to strings) are sent with the encoding specified by . + /// + public static ResponseSerializerCallback None(bool bufferResponse) + => bufferResponse ? BufferingBaseSerializer : ChunkedEncodingBaseSerializer; + + private static ResponseSerializerCallback GetBaseSerializer(bool bufferResponse) + => async (context, data) => { + if (data is null) + { + return; + } + + var isBinaryResponse = data is byte[]; + + if (!context.TryDetermineCompression(context.Response.ContentType, out var preferCompression)) + { + preferCompression = true; + } + + if (isBinaryResponse) + { + var responseBytes = (byte[])data; + using var stream = context.OpenResponseStream(bufferResponse, preferCompression); + await stream.WriteAsync(responseBytes, 0, responseBytes.Length).ConfigureAwait(false); + } + else + { + var responseString = data is string stringData ? stringData : data.ToString() ?? string.Empty; + using var text = context.OpenResponseText(context.Response.ContentEncoding, bufferResponse, preferCompression); + await text.WriteAsync(responseString).ConfigureAwait(false); + } + }; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/ResponseSerializerCallback.cs b/Vendor/EmbedIO-3.5.2/ResponseSerializerCallback.cs new file mode 100644 index 0000000..f84cca1 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/ResponseSerializerCallback.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace EmbedIO +{ + /// + /// A callback used to serialize data to a HTTP response. + /// + /// The HTTP context of the request. + /// The data to serialize. + /// A representing the ongoing operation. + public delegate Task ResponseSerializerCallback(IHttpContext context, object? data); +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/BaseRouteAttribute.cs b/Vendor/EmbedIO-3.5.2/Routing/BaseRouteAttribute.cs new file mode 100644 index 0000000..df449fb --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/BaseRouteAttribute.cs @@ -0,0 +1,31 @@ +using System; + +namespace EmbedIO.Routing +{ + /// + /// Decorate methods within controllers with this attribute in order to make them callable from the Web API Module + /// Method Must match the WebServerModule. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class BaseRouteAttribute : RouteAttribute + { + /// + /// Initializes a new instance of the class. + /// + /// The verb. + /// The route. + /// is . + /// + /// is empty. + /// - or - + /// does not start with a slash (/) character. + /// - or - + /// does not comply with route syntax. + /// + /// + public BaseRouteAttribute(HttpVerbs verb, string route) + : base(verb, route, true) + { + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/Route.cs b/Vendor/EmbedIO-3.5.2/Routing/Route.cs new file mode 100644 index 0000000..f02fdf2 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/Route.cs @@ -0,0 +1,310 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using EmbedIO.WebApi; + +namespace EmbedIO.Routing +{ + /// + /// Provides utility methods to work with routes. + /// + /// + /// + /// + public static class Route + { + // Characters in ValidParameterNameChars MUST be in ascending ordinal order! + private static readonly char[] ValidParameterNameChars = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".ToCharArray(); + + /// + /// Determines whether a string is a valid route parameter name. + /// To be considered a valid route parameter name, the specified string: + /// + /// must not be ; + /// must not be the empty string; + /// must consist entirely of decimal digits, upper- or lower-case + /// letters of the English alphabet, or underscore ('_') characters; + /// must not start with a decimal digit. + /// + /// + /// The value. + /// if is a valid route parameter; + /// otherwise, . + public static bool IsValidParameterName(string value) + => !string.IsNullOrEmpty(value) + && value[0] > '9' + && !value.Any(c => c < '0' || c > 'z' || Array.BinarySearch(ValidParameterNameChars, c) < 0); + + /// + /// Determines whether a string is a valid route. + /// To be considered a valid route, the specified string: + /// + /// must not be ; + /// must not be the empty string; + /// must start with a slash ('/') character; + /// if a base route, must end with a slash ('/') character; + /// if not a base route, must not end with a slash ('/') character, + /// unless it is the only character in the string; + /// must not contain consecutive runs of two or more slash ('/') characters; + /// may contain one or more parameter specifications. + /// + /// Each parameter specification must be enclosed in curly brackets ('{' + /// and '}'. No whitespace is allowed inside a parameter specification. + /// Two parameter specifications must be separated by literal text. + /// A parameter specification consists of a valid parameter name, optionally + /// followed by a '?' character to signify that it will also match an empty string. + /// If '?' is not present, a parameter by default will NOT match an empty string. + /// See for the definition of a valid parameter name. + /// To include a literal open curly bracket in the route, it must be doubled ("{{"). + /// A literal closed curly bracket ('}') may be included in the route as-is. + /// A segment of a base route cannot consist only of an optional parameter. + /// + /// The route to check. + /// if checking for a base route; + /// otherwise, . + /// if is a valid route; + /// otherwise, . + public static bool IsValid(string route, bool isBaseRoute) => ValidateInternal(nameof(route), route, isBaseRoute) == null; + + // Check the validity of a route by parsing it without storing the results. + // Returns: ArgumentNullException, ArgumentException, null if OK + internal static Exception? ValidateInternal(string argumentName, string value, bool isBaseRoute) => ParseInternal(value, isBaseRoute, null) switch { + ArgumentNullException _ => new ArgumentNullException(argumentName), + FormatException formatException => new ArgumentException(formatException.Message, argumentName), + Exception exception => exception, + _ => null + }; + + // Validate and parse a route, constructing a Regex pattern. + // setResult will be called at the end with the isBaseRoute flag, parameter names and the constructed pattern. + // Returns: ArgumentNullException, FormatException, null if OK + internal static Exception? ParseInternal(string route, bool isBaseRoute, Action, string>? setResult) + { + if (route == null) + return new ArgumentNullException(nameof(route)); + + if (route.Length == 0) + return new FormatException("Route is empty."); + + if (route[0] != '/') + return new FormatException("Route does not start with a slash."); + + /* + * Regex options set at start of pattern: + * IgnoreCase : no + * Multiline : no + * Singleline : yes + * ExplicitCapture : yes + * IgnorePatternWhitespace : no + * See https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-options + * See https://docs.microsoft.com/en-us/dotnet/standard/base-types/grouping-constructs-in-regular-expressions#group_options + */ + const string InitialRegexOptions = "(?sn-imx)"; + + // If setResult is null we don't need the StringBuilder. + var sb = setResult == null ? null : new StringBuilder("^"); + + var parameterNames = new List(); + if (route.Length == 1) + { + // If the route consists of a single slash, only a single slash will match. + sb?.Append(isBaseRoute ? "/" : "/$"); + } + else + { + // First of all divide the route in segments. + // Segments are separated by slashes. + // The route is not necessarily normalized, so there could be runs of consecutive slashes. + var segmentCount = 0; + var optionalSegmentCount = 0; + foreach (var segment in GetSegments(route)) + { + segmentCount++; + + // Parse the segment, looking alternately for a '{', that opens a parameter specification, + // then for a '}', that closes it. + // Characters outside parameter specifications are Regex-escaped and added to the pattern. + // A parameter specification consists of a parameter name, optionally followed by '?' + // to indicate that an empty parameter will match. + // The default is to NOT match empty parameters, consistently with ASP.NET and EmbedIO version 2. + // More syntax rules: + // - There cannot be two parameters without literal text in between. + // - If a segment consists ONLY of an OPTIONAL parameter, then the slash preceding it is optional too. + var inParameterSpec = false; + var afterParameter = false; + for (var position = 0; ;) + { + if (inParameterSpec) + { + // Look for end of spec, bail out if not found. + var closePosition = segment.IndexOf('}', position); + if (closePosition < 0) + return new FormatException("Route syntax error: unclosed parameter specification."); + + // Parameter spec cannot be empty. + if (closePosition == position) + return new FormatException("Route syntax error: empty parameter specification."); + + // Check the last character: + // {name} means empty parameter does not match + // {name?} means empty parameter matches + // If '?'is found, the parameter name ends before it + var nameEndPosition = closePosition; + var allowEmpty = false; + if (segment[closePosition - 1] == '?') + { + allowEmpty = true; + nameEndPosition--; + } + + // Bail out if only '?' is found inside the spec. + if (nameEndPosition == position) + return new FormatException("Route syntax error: missing parameter name."); + + // Extract the parameter name. + var parameterName = segment.Substring(position, nameEndPosition - position); + + // Ensure that the parameter name contains only valid characters. + if (!IsValidParameterName(parameterName)) + return new FormatException("Route syntax error: parameter name contains one or more invalid characters."); + + // Ensure that the parameter name is not a duplicate. + if (parameterNames.Contains(parameterName)) + return new FormatException("Route syntax error: duplicate parameter name."); + + // The spec is valid, so add the parameter to the list. + parameterNames.Add(parameterName); + + // Append a capturing group with the same name to the pattern. + // Parameters must be made of non-slash characters ("[^/]") + // and must match non-greedily ("*?" if optional, "+?" if non optional). + // Position will be 1 at the start, not 0, because we've skipped the opening '{'. + if (allowEmpty && position == 1 && closePosition == segment.Length - 1) + { + if (isBaseRoute) + return new FormatException("No segment of a base route can be optional."); + + // If the segment consists only of an optional parameter, + // then the slash preceding the segment is optional as well. + // In this case the parameter must match only is not empty, + // because it's (slash + parameter) that is optional. + sb?.Append("(/(?<").Append(parameterName).Append(">[^/]+?))?"); + optionalSegmentCount++; + } + else + { + // If at the start of a segment, don't forget the slash! + // Position will be 1 at the start, not 0, because we've skipped the opening '{'. + if (position == 1) + sb?.Append('/'); + + sb?.Append("(?<").Append(parameterName).Append(">[^/]").Append(allowEmpty ? '*' : '+').Append("?)"); + } + + // Go on with parsing. + position = closePosition + 1; + inParameterSpec = false; + afterParameter = true; + } + else + { + // Look for start of parameter spec. + var openPosition = segment.IndexOf('{', position); + if (openPosition < 0) + { + // If at the start of a segment, don't forget the slash. + if (position == 0) + sb?.Append('/'); + + // No more parameter specs: escape the remainder of the string + // and add it to the pattern. + sb?.Append(Regex.Escape(segment.Substring(position))); + break; + } + + var nextPosition = openPosition + 1; + if (nextPosition < segment.Length && segment[nextPosition] == '{') + { + // If another identical char follows, treat the two as a single literal char. + // If at the start of a segment, don't forget the slash! + if (position == 0) + sb?.Append('/'); + + sb?.Append(@"\\{"); + } + else if (afterParameter && openPosition == position) + { + // If a parameter immediately follows another parameter, + // with no literal text in between, it's a syntax error. + return new FormatException("Route syntax error: parameters must be separated by literal text."); + } + else + { + // If at the start of a segment, don't forget the slash, + // but only if there actually is some literal text. + // Otherwise let the parameter spec parsing code deal with the slash, + // because we don't know whether this is an optional segment yet. + if (position == 0 && openPosition > 0) + sb?.Append('/'); + + // Escape the part of the pattern outside the parameter spec + // and add it to the pattern. + sb?.Append(Regex.Escape(segment.Substring(position, openPosition - position))); + inParameterSpec = true; + } + + // Go on parsing. + position = nextPosition; + afterParameter = false; + } + } + } + + // Close the pattern + sb?.Append(isBaseRoute ? "(/|$)" : "$"); + + // If all segments are optional segments, "/" must match too. + if (optionalSegmentCount == segmentCount) + sb?.Insert(0, "(/$)|(").Append(')'); + } + + // Pass the results to the callback if needed. + setResult?.Invoke(isBaseRoute, parameterNames, InitialRegexOptions + sb); + + // Everything's fine, thus no exception. + return null; + } + + // Enumerate the segments of a route, ignoring consecutive slashes. + private static IEnumerable GetSegments(string route) + { + var length = route.Length; + var position = 0; + for (; ; ) + { + while (route[position] == '/') + { + position++; + if (position >= length) + break; + } + + if (position >= length) + break; + + var slashPosition = route.IndexOf('/', position); + if (slashPosition < 0) + { + yield return route.Substring(position); + break; + } + + yield return route.Substring(position, slashPosition - position); + position = slashPosition; + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/RouteAttribute.cs b/Vendor/EmbedIO-3.5.2/Routing/RouteAttribute.cs new file mode 100644 index 0000000..3705fd7 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/RouteAttribute.cs @@ -0,0 +1,54 @@ +using System; + +namespace EmbedIO.Routing +{ + /// + /// Decorate methods within controllers with this attribute in order to make them callable from the Web API Module + /// Method Must match the WebServerModule. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class RouteAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// if this attribute represents a base route; + /// (the default) if it represents a terminal (non-base) route. + /// The verb. + /// The route. + /// is . + /// + /// is empty. + /// - or - + /// does not start with a slash (/) character. + /// - or - + /// does not comply with route syntax. + /// + /// + public RouteAttribute(HttpVerbs verb, string route, bool isBaseRoute = false) + { + Matcher = RouteMatcher.Parse(route, isBaseRoute); + Verb = verb; + } + + /// + /// Gets the HTTP verb handled by a method with this attribute. + /// + public HttpVerbs Verb { get; } + + /// + /// Gets a that will match URLs against this attribute's data. + /// + public RouteMatcher Matcher { get; } + + /// + /// Gets the route handled by a method with this attribute. + /// + public string Route => Matcher.Route; + + /// + /// Gets a value indicating whether this attribute represents a base route. + /// + public bool IsBaseRoute => Matcher.IsBaseRoute; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/RouteHandlerCallback.cs b/Vendor/EmbedIO-3.5.2/Routing/RouteHandlerCallback.cs new file mode 100644 index 0000000..bd5076b --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/RouteHandlerCallback.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace EmbedIO.Routing +{ + /// + /// Base class for callbacks used to handle routed requests. + /// + /// An interface representing the context of the request. + /// The matched route. + /// A representing the ongoing operation. + /// + public delegate Task RouteHandlerCallback(IHttpContext context, RouteMatch route); +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/RouteMatch.cs b/Vendor/EmbedIO-3.5.2/Routing/RouteMatch.cs new file mode 100644 index 0000000..38e3d49 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/RouteMatch.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using EmbedIO.Utilities; + +namespace EmbedIO.Routing +{ + /// + /// Represents a route resolved by a . + /// This class may be used both as a dictionary of route parameter names and values, + /// and a list of the values. + /// Because of its double nature, this class cannot be enumerated directly. However, + /// you may use the property to iterate over name / value pairs, and the + /// property to iterate over values. + /// When enumerated in a non-generic fashion via the interface, + /// this class iterates over name / value pairs. + /// +#pragma warning disable CA1710 // Rename class to end in "Collection" + public sealed class RouteMatch : IReadOnlyList, IReadOnlyDictionary +#pragma warning restore CA1710 + { + private static readonly IReadOnlyList EmptyStringList = Array.Empty(); + + private readonly IReadOnlyList _values; + + internal RouteMatch(string path, IReadOnlyList names, IReadOnlyList values, string? subPath) + { + Path = path; + Names = names; + _values = values; + SubPath = subPath; + } + + /// + /// Gets a instance that represents no match at all. + /// + /// + /// The instance returned by this property + /// has the following specifications: + /// + /// its Path property is the empty string; + /// it has no parameters; + /// its SubPath property is . + /// + /// This instance is only useful to initialize + /// a non-nullable property of type , provided that it is subsequently + /// set to a meaningful value before being used. + /// + public static RouteMatch None { get; } = new RouteMatch( + string.Empty, + Array.Empty(), + Array.Empty(), + null); + + /// + /// Gets the URL path that was successfully matched against the route. + /// + public string Path { get; } + + /// + /// For a base route, gets the part of that follows the matched route; + /// for a non-base route, this property is always . + /// + public string? SubPath { get; } + + /// + /// Gets a list of the names of the route's parameters. + /// + public IReadOnlyList Names { get; } + + /// + public int Count => _values.Count; + + /// + public IEnumerable Keys => Names; + + /// + public IEnumerable Values => _values; + + /// + /// Gets an interface that can be used + /// to iterate over name / value pairs. + /// + public IEnumerable> Pairs => this; + + /// + public string this[int index] => _values[index]; + + /// + public string this[string key] + { + get + { + var count = Names.Count; + for (var i = 0; i < count; i++) + { + if (Names[i] == key) + { + return _values[i]; + } + } + + throw new KeyNotFoundException("The parameter name was not found."); + } + } + + /// + /// Returns a object equal to the one + /// that would result by matching the specified URL path against a + /// base route of "/". + /// + /// The URL path to match. + /// A newly-constructed . + /// + /// This method assumes that + /// is a valid, non-base URL path or route. Otherwise, the behavior of this method + /// is unspecified. + /// Ensure that you validate before + /// calling this method, using either + /// or . + /// + public static RouteMatch UnsafeFromRoot(string urlPath) + => new RouteMatch(urlPath, EmptyStringList, EmptyStringList, urlPath); + + /// + /// Returns a object equal to the one + /// that would result by matching the specified URL path against + /// the specified parameterless base route. + /// + /// The base route to match against. + /// The URL path to match. + /// A newly-constructed . + /// + /// This method assumes that is a + /// valid base URL path, and + /// is a valid, non-base URL path or route. Otherwise, the behavior of this method + /// is unspecified. + /// Ensure that you validate both parameters before + /// calling this method, using either + /// or . + /// + public static RouteMatch? UnsafeFromBasePath(string baseUrlPath, string urlPath) + { + var subPath = UrlPath.UnsafeStripPrefix(urlPath, baseUrlPath); + return subPath == null ? null : new RouteMatch(urlPath, EmptyStringList, EmptyStringList, "/" + subPath); + } + + /// + public bool ContainsKey(string key) => Names.Any(n => n == key); + + /// + public bool TryGetValue(string key, out string? value) + { + var count = Names.Count; + for (var i = 0; i < count; i++) + { + if (Names[i] == key) + { + value = _values[i]; + return true; + } + } + + value = null; + return false; + } + + /// + /// Returns the index of the parameter with the specified name. + /// + /// The parameter name. + /// The index of the parameter, or -1 if none of the + /// route parameters have the specified name. + public int IndexOf(string name) + { + var count = Names.Count; + for (var i = 0; i < count; i++) + { + if (Names[i] == name) + { + return i; + } + } + + return -1; + } + + /// + IEnumerator> IEnumerable>.GetEnumerator() + => Names.Zip(_values, (n, v) => new KeyValuePair(n, v)).GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => _values.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => Pairs.GetEnumerator(); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/RouteMatcher.cs b/Vendor/EmbedIO-3.5.2/Routing/RouteMatcher.cs new file mode 100644 index 0000000..d66ec96 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/RouteMatcher.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using EmbedIO.Utilities; +using Swan; + +namespace EmbedIO.Routing +{ + /// + /// Matches URL paths against a route. + /// + public sealed class RouteMatcher : IEquatable + { + private static readonly object SyncRoot = new object(); + private static readonly Dictionary<(bool, string), RouteMatcher> Cache = new Dictionary<(bool, string), RouteMatcher>(); + + private readonly Regex _regex; + + private RouteMatcher(bool isBaseRoute, string route, string pattern, IReadOnlyList parameterNames) + { + IsBaseRoute = isBaseRoute; + Route = route; + ParameterNames = parameterNames; + _regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.CultureInvariant); + } + + /// + /// Gets a value indicating whether the property + /// is a base route. + /// + public bool IsBaseRoute { get; } + + /// + /// Gets the route this instance matches URL paths against. + /// + public string Route { get; } + + /// + /// Gets the names of the route's parameters. + /// + public IReadOnlyList ParameterNames { get; } + + /// + /// Constructs an instance of by parsing the specified route. + /// If the same route was previously parsed and the method has not been called since, + /// this method obtains an instance from a static cache. + /// + /// The route to parse. + /// if the route to parse + /// is a base route; otherwise, . + /// A newly-constructed instance of + /// that will match URL paths against . + /// is . + /// is not a valid route. + /// + /// + public static RouteMatcher Parse(string route, bool isBaseRoute) + { + var exception = TryParseInternal(route, isBaseRoute, out var result); + if (exception != null) + throw exception; + + return result!; + } + + /// + /// Attempts to obtain an instance of by parsing the specified route. + /// If the same route was previously parsed and the method has not been called since, + /// this method obtains an instance from a static cache. + /// + /// The route to parse. + /// if the route to parse + /// is a base route; otherwise, . + /// When this method returns , a newly-constructed instance of + /// that will match URL paths against ; otherwise, . + /// This parameter is passed uninitialized. + /// if parsing was successful; otherwise, . + /// + /// + public static bool TryParse(string route, bool isBaseRoute, out RouteMatcher? result) + => TryParseInternal(route, isBaseRoute, out result) == null; + + /// + /// Clears 's internal instance cache. + /// + /// + /// + public static void ClearCache() + { + lock (SyncRoot) + { + Cache.Clear(); + } + } + + /// + /// Returns a hash code for this instance. + /// + /// A hash code for this instance, suitable for use in hashing algorithms + /// and data structures like a hash table. + public override int GetHashCode() => CompositeHashCode.Using(Route, IsBaseRoute); + + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with this instance. + /// if is equal to this instance; + /// otherwise, . + public override bool Equals(object? obj) => obj is RouteMatcher other && Equals(other); + + /// + /// Indicates whether this instance is equal to another instance of . + /// + /// A to compare with this instance. + /// if this instance is equal to ; + /// otherwise, . + public bool Equals(RouteMatcher? other) + => other != null + && other.Route == Route + && other.IsBaseRoute == IsBaseRoute; + + /// + /// Matches the specified URL path against + /// and extracts values for the route's parameters. + /// + /// The URL path to match. + /// If the match is successful, a object; + /// otherwise, . + public RouteMatch? Match(string path) + { + if (path == null) + return null; + + // Optimize for parameterless base routes + if (IsBaseRoute) + { + if (Route.Length == 1) + return RouteMatch.UnsafeFromRoot(path); + + if (ParameterNames.Count == 0) + return RouteMatch.UnsafeFromBasePath(Route, path); + } + + var match = _regex.Match(path); + if (!match.Success) + return null; + + return new RouteMatch( + path, + ParameterNames, + match.Groups.Cast().Skip(1).Select(g => WebUtility.UrlDecode(g.Value)).ToArray(), + IsBaseRoute ? "/" + path.Substring(match.Groups[0].Length) : null); + } + + private static Exception? TryParseInternal(string route, bool isBaseRoute, out RouteMatcher? result) + { + lock (SyncRoot) + { + string? pattern = null; + var parameterNames = new List(); + var exception = Routing.Route.ParseInternal(route, isBaseRoute, (_, n, p) => { + parameterNames.AddRange(n); + pattern = p; + }); + if (exception != null) + { + result = null; + return exception; + } + + route = UrlPath.UnsafeNormalize(route, isBaseRoute); + if (Cache.TryGetValue((isBaseRoute, route), out result)) + return null; + + result = new RouteMatcher(isBaseRoute, route, pattern!, parameterNames); + Cache.Add((isBaseRoute, route), result); + return null; + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/RouteResolutionResult.cs b/Vendor/EmbedIO-3.5.2/Routing/RouteResolutionResult.cs new file mode 100644 index 0000000..198d059 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/RouteResolutionResult.cs @@ -0,0 +1,35 @@ +namespace EmbedIO.Routing +{ + /// + /// Represents the outcome of resolving a context and a path against a route. + /// + public enum RouteResolutionResult + { + /* DO NOT reorder members! + * RouteNotMatched < NoHandlerSelected < NoHandlerSuccessful < Success + * + * See comments in RouteResolverBase<,>.ResolveAsync for further explanation. + */ + + /// + /// The route didn't match. + /// + RouteNotMatched, + + /// + /// The route did match, but no registered handler was suitable for the context. + /// + NoHandlerSelected, + + /// + /// The route matched and one or more suitable handlers were found, + /// but none of them returned . + /// + NoHandlerSuccessful, + + /// + /// The route has been resolved. + /// + Success, + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/RouteResolverBase`1.cs b/Vendor/EmbedIO-3.5.2/Routing/RouteResolverBase`1.cs new file mode 100644 index 0000000..1556324 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/RouteResolverBase`1.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using EmbedIO.Internal; +using EmbedIO.Utilities; +using Swan.Configuration; + +namespace EmbedIO.Routing +{ + /// + /// Implements the logic for resolving the requested path of a HTTP context against a route, + /// possibly handling different contexts via different handlers. + /// + /// The type of the data used to select a suitable handler + /// for the context. + /// + public abstract class RouteResolverBase : ConfiguredObject + { + private readonly List<(TData data, RouteHandlerCallback handler)> _dataHandlerPairs + = new List<(TData data, RouteHandlerCallback handler)>(); + + /// + /// Initializes a new instance of the class. + /// + /// The to match URL paths against. + /// + /// is . + /// + protected RouteResolverBase(RouteMatcher matcher) + { + Matcher = Validate.NotNull(nameof(matcher), matcher); + } + + /// + /// Gets the used to match routes. + /// + public RouteMatcher Matcher { get; } + + /// + /// Gets the route this resolver matches URL paths against. + /// + public string Route => Matcher.Route; + + /// + /// Gets a value indicating whether is a base route. + /// + public bool IsBaseRoute => Matcher.IsBaseRoute; + + /// + /// Associates some data to a handler. + /// The method calls + /// to extract data from the context; then, for each registered data / handler pair, + /// is called to determine whether + /// should be called. + /// + /// Data used to determine which contexts are + /// suitable to be handled by . + /// A callback used to handle matching contexts. + /// is . + /// + /// + /// + /// + public void Add(TData data, RouteHandlerCallback handler) + { + EnsureConfigurationNotLocked(); + + handler = Validate.NotNull(nameof(handler), handler); + _dataHandlerPairs.Add((data, handler)); + } + + /// + /// Associates some data to a synchronous handler. + /// The method calls + /// to extract data from the context; then, for each registered data / handler pair, + /// is called to determine whether + /// should be called. + /// + /// Data used to determine which contexts are + /// suitable to be handled by . + /// A callback used to handle matching contexts. + /// is . + /// + /// + /// + /// + public void Add(TData data, SyncRouteHandlerCallback handler) + { + EnsureConfigurationNotLocked(); + + handler = Validate.NotNull(nameof(handler), handler); + _dataHandlerPairs.Add((data, (ctx, route) => { + handler(ctx, route); + return Task.CompletedTask; + })); + } + + /// + /// Locks this instance, preventing further handler additions. + /// + public void Lock() => LockConfiguration(); + + /// + /// Asynchronously matches a URL path against ; + /// if the match is successful, tries to handle the specified + /// using handlers selected according to data extracted from the context. + /// Registered data / handler pairs are tried in the same order they were added. + /// + /// The context to handle. + /// A , representing the ongoing operation, + /// that will return a result in the form of one of the constants. + /// + /// + /// + /// + public async Task ResolveAsync(IHttpContext context) + { + LockConfiguration(); + + var match = Matcher.Match(context.RequestedPath); + if (match == null) + return RouteResolutionResult.RouteNotMatched; + + var contextData = GetContextData(context); + var result = RouteResolutionResult.NoHandlerSelected; + foreach (var (data, handler) in _dataHandlerPairs) + { + if (!MatchContextData(contextData, data)) + continue; + + try + { + await handler(context, match).ConfigureAwait(false); + return RouteResolutionResult.Success; + } + catch (RequestHandlerPassThroughException) + { + result = RouteResolutionResult.NoHandlerSuccessful; + } + } + + return result; + } + + /// + /// Called by to extract data from a context. + /// The extracted data are then used to select which handlers are suitable + /// to handle the context. + /// + /// The HTTP context to extract data from. + /// The extracted data. + /// + /// + protected abstract TData GetContextData(IHttpContext context); + + /// + /// Called by to match data extracted from a context + /// against data associated with a handler. + /// + /// The data extracted from the context. + /// The data associated with the handler. + /// if the handler should be called to handle the context; + /// otherwise, . + protected abstract bool MatchContextData(TData contextData, TData handlerData); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/RouteResolverCollectionBase`2.cs b/Vendor/EmbedIO-3.5.2/Routing/RouteResolverCollectionBase`2.cs new file mode 100644 index 0000000..7795ff5 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/RouteResolverCollectionBase`2.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using EmbedIO.Internal; +using EmbedIO.Utilities; +using Swan; +using Swan.Collections; +using Swan.Configuration; + +namespace EmbedIO.Routing +{ + /// + /// Implements the logic for resolving a context and a URL path against a list of routes, + /// possibly handling different HTTP methods via different handlers. + /// + /// The type of the data used to select a suitable handler + /// for a context. + /// The type of the route resolver. + /// + public abstract class RouteResolverCollectionBase : ConfiguredObject + where TResolver : RouteResolverBase + { + private readonly List _resolvers = new List(); + + /// + /// Associates some data and a route to a handler. + /// + /// Data used to determine which contexts are + /// suitable to be handled by . + /// The to match URL paths against. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + /// The method + /// returned . + /// + /// + /// + public void Add(TData data, RouteMatcher matcher, RouteHandlerCallback handler) + { + matcher = Validate.NotNull(nameof(matcher), matcher); + handler = Validate.NotNull(nameof(handler), handler); + GetResolver(matcher).Add(data, handler); + } + + /// + /// Associates some data and a route to a synchronous handler. + /// + /// Data used to determine which contexts are + /// suitable to be handled by . + /// The to match URL paths against. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + /// The method + /// returned . + /// + /// + /// + public void Add(TData data, RouteMatcher matcher, SyncRouteHandlerCallback handler) + { + matcher = Validate.NotNull(nameof(matcher), matcher); + handler = Validate.NotNull(nameof(handler), handler); + GetResolver(matcher).Add(data, handler); + } + + /// + /// Asynchronously matches a URL path against ; + /// if the match is successful, tries to handle the specified + /// using handlers selected according to data extracted from the context. + /// Registered resolvers are tried in the same order they were added by calling + /// . + /// + /// The context to handle. + /// A , representing the ongoing operation, + /// that will return a result in the form of one of the constants. + /// + public async Task ResolveAsync(IHttpContext context) + { + var result = RouteResolutionResult.RouteNotMatched; + foreach (var resolver in _resolvers) + { + var resolverResult = await resolver.ResolveAsync(context).ConfigureAwait(false); + OnResolverCalled(context, resolver, resolverResult); + if (resolverResult == RouteResolutionResult.Success) + return RouteResolutionResult.Success; + + // This is why RouteResolutionResult constants must not be reordered. + if (resolverResult > result) + result = resolverResult; + } + + return result; + } + + /// + /// Locks this collection, preventing further additions. + /// + public void Lock() => LockConfiguration(); + + /// + protected override void OnBeforeLockConfiguration() + { + foreach (var resolver in _resolvers) + resolver.Lock(); + } + + /// + /// Called by + /// and to create an instance + /// of that can resolve the specified route. + /// If this method returns , an + /// is thrown by the calling method. + /// + /// The to match URL paths against. + /// A newly-constructed instance of . + protected abstract TResolver CreateResolver(RouteMatcher matcher); + + /// + /// Called by when a resolver's + /// ResolveAsync method has been called + /// to resolve a context. + /// This callback method may be used e.g. for logging or testing. + /// + /// The context to handle. + /// The resolver just called. + /// The result returned by .ResolveAsync. + protected virtual void OnResolverCalled(IHttpContext context, TResolver resolver, RouteResolutionResult result) + { + } + + private TResolver GetResolver(RouteMatcher matcher) + { + var resolver = _resolvers.FirstOrDefault(r => r.Matcher.Equals(matcher)); + if (resolver != null) + return resolver; + + resolver = CreateResolver(matcher); + _resolvers.Add(resolver ?? throw SelfCheck.Failure($"{nameof(CreateResolver)} returned null.")); + return resolver; + } + } +} diff --git a/Vendor/EmbedIO-3.5.2/Routing/RouteVerbResolver.cs b/Vendor/EmbedIO-3.5.2/Routing/RouteVerbResolver.cs new file mode 100644 index 0000000..82e7b5a --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/RouteVerbResolver.cs @@ -0,0 +1,30 @@ +using System; + +namespace EmbedIO.Routing +{ + /// + /// Handles a HTTP request by matching it against a route, + /// possibly handling different HTTP methods via different handlers. + /// + public sealed class RouteVerbResolver : RouteResolverBase + { + /// + /// Initializes a new instance of the class. + /// + /// The to match URL paths against. + /// + /// is . + /// + public RouteVerbResolver(RouteMatcher matcher) + : base(matcher) + { + } + + /// + protected override HttpVerbs GetContextData(IHttpContext context) => context.Request.HttpVerb; + + /// + protected override bool MatchContextData(HttpVerbs contextVerb, HttpVerbs handlerVerb) + => handlerVerb == HttpVerbs.Any || contextVerb == handlerVerb; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/RouteVerbResolverCollection.cs b/Vendor/EmbedIO-3.5.2/Routing/RouteVerbResolverCollection.cs new file mode 100644 index 0000000..d3f97f2 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/RouteVerbResolverCollection.cs @@ -0,0 +1,140 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using EmbedIO.Utilities; +using Swan.Logging; + +namespace EmbedIO.Routing +{ + /// + /// Handles a HTTP request by matching it against a list of routes, + /// possibly handling different HTTP methods via different handlers. + /// + /// + /// + public sealed class RouteVerbResolverCollection : RouteResolverCollectionBase + { + private readonly string _logSource; + + internal RouteVerbResolverCollection(string logSource) + { + _logSource = logSource; + } + + /// + /// Adds handlers, associating them with HTTP method / route pairs by means + /// of Route attributes. + /// A compatible handler is a static or instance method that takes 2 + /// parameters having the following types, in order: + /// + /// + /// + /// + /// The return type of a compatible handler may be either + /// or . + /// A compatible handler, in order to be added to a , + /// must have one or more Route attributes. + /// The same handler will be added once for each such attribute, either declared on the handler, + /// or inherited (if the handler is a virtual method). + /// This method behaves according to the type of the + /// parameter: + /// + /// if is a , all public static methods of + /// the type (either declared on the same type or inherited) that are compatible handlers will be added + /// to the collection; + /// if is an , all public static methods of + /// each exported type of the assembly (either declared on the same type or inherited) that are compatible handlers will be added + /// to the collection; + /// if is a referring to a compatible handler, + /// it will be added to the collection; + /// if is a whose Method + /// refers to a compatible handler, that method will be added to the collection; + /// if is none of the above, all public instance methods of + /// its type (either declared on the same type or inherited) that are compatible handlers will be bound to + /// and added to the collection. + /// + /// + /// Where to look for compatible handlers. See the Summary section for more information. + /// + /// The number of handlers that were added to the collection. + /// Note that methods with multiple Route attributes + /// will count as one for each attribute. + /// + /// is . + public int AddFrom(object target) => Validate.NotNull(nameof(target), target) switch { + Type type => AddFrom(null, type), + Assembly assembly => assembly.GetExportedTypes().Sum(t => AddFrom(null, t)), + MethodInfo method => method.IsStatic ? Add(null, method) : 0, + Delegate callback => Add(callback.Target, callback.Method), + _ => AddFrom(target, target.GetType()) + }; + + /// + protected override RouteVerbResolver CreateResolver(RouteMatcher matcher) => new RouteVerbResolver(matcher); + + /// + protected override void OnResolverCalled(IHttpContext context, RouteVerbResolver resolver, RouteResolutionResult result) + => $"[{context.Id}] Route {resolver.Route} : {result}".Trace(_logSource); + + private static bool IsHandlerCompatibleMethod(MethodInfo method, out bool isSynchronous) + { + isSynchronous = false; + var returnType = method.ReturnType; + if (returnType == typeof(void)) + { + isSynchronous = true; + } + else if (returnType != typeof(Task)) + { + return false; + } + + var parameters = method.GetParameters(); + return parameters.Length == 2 + && parameters[0].ParameterType.IsAssignableFrom(typeof(IHttpContext)) + && parameters[1].ParameterType.IsAssignableFrom(typeof(RouteMatch)); + } + + // Call Add with all suitable methods of a Type, return sum of results. + private int AddFrom(object? target, Type type) + => type.GetMethods(target == null + ? BindingFlags.Public | BindingFlags.Static + : BindingFlags.Public | BindingFlags.Instance) + .Where(method => method.IsPublic + && !method.IsAbstract + && !method.ContainsGenericParameters) + .Sum(m => Add(target, m)); + + private int Add(object? target, MethodInfo method) + { + if (!IsHandlerCompatibleMethod(method, out var isSynchronous)) + return 0; + + var attributes = method.GetCustomAttributes(true).OfType().ToArray(); + if (attributes.Length == 0) + return 0; + + var parameters = new[] { + Expression.Parameter(typeof(IHttpContext), "context"), + Expression.Parameter(typeof(RouteMatch), "route"), + }; + + Expression body = Expression.Call(Expression.Constant(target), method, parameters.Cast()); + if (isSynchronous) + { + // Convert void to Task by evaluating Task.CompletedTask + body = Expression.Block(typeof(Task), body, Expression.Constant(Task.CompletedTask)); + } + + var handler = Expression.Lambda(body, parameters).Compile(); + foreach (var attribute in attributes) + { + Add(attribute.Verb, attribute.Matcher, handler); + } + + return attributes.Length; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/RoutingModule.cs b/Vendor/EmbedIO-3.5.2/Routing/RoutingModule.cs new file mode 100644 index 0000000..2875677 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/RoutingModule.cs @@ -0,0 +1,62 @@ +using System; + +namespace EmbedIO.Routing +{ + /// + /// A module that handles requests by resolving route / method pairs associated with handlers. + /// + /// + public class RoutingModule : RoutingModuleBase + { + /// + /// + /// Initializes a new instance of the class. + /// + public RoutingModule(string baseRoute) + : base(baseRoute) + { + } + + /// + /// Associates a HTTP method and a route to a handler. + /// + /// A constant representing the HTTP method + /// to associate with , or + /// if can handle all HTTP methods. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + public void Add(HttpVerbs verb, RouteMatcher matcher, RouteHandlerCallback handler) + => AddHandler(verb, matcher, handler); + + /// + /// Associates a HTTP method and a route to a synchronous handler. + /// + /// A constant representing the HTTP method + /// to associate with , or + /// if can handle all HTTP methods. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + public void Add(HttpVerbs verb, RouteMatcher matcher, SyncRouteHandlerCallback handler) + => AddHandler(verb, matcher, handler); + + /// + /// Adds handlers, associating them with HTTP method / route pairs by means + /// of Route attributes. + /// See for further information. + /// + /// Where to look for compatible handlers. + /// The number of handlers that were added. + /// is . + public int AddFrom(object target) => AddHandlersFrom(target); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/RoutingModuleBase.cs b/Vendor/EmbedIO-3.5.2/Routing/RoutingModuleBase.cs new file mode 100644 index 0000000..56e9cc9 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/RoutingModuleBase.cs @@ -0,0 +1,317 @@ +using System; +using System.Threading.Tasks; +using Swan; + +namespace EmbedIO.Routing +{ + /// + /// Base class for modules that handle requests by resolving route / method pairs associated with handlers. + /// + /// + public abstract class RoutingModuleBase : WebModuleBase + { + private readonly RouteVerbResolverCollection _resolvers = new RouteVerbResolverCollection(nameof(RoutingModuleBase)); + + /// + /// + /// Initializes a new instance of the class. + /// + protected RoutingModuleBase(string baseRoute) + : base(baseRoute) + { + } + + /// + public override bool IsFinalHandler => true; + + /// + protected override async Task OnRequestAsync(IHttpContext context) + { + var result = await _resolvers.ResolveAsync(context).ConfigureAwait(false); + switch (result) + { + case RouteResolutionResult.RouteNotMatched: + case RouteResolutionResult.NoHandlerSuccessful: + await OnPathNotFoundAsync(context).ConfigureAwait(false); + break; + case RouteResolutionResult.NoHandlerSelected: + await OnMethodNotAllowedAsync(context).ConfigureAwait(false); + break; + case RouteResolutionResult.Success: + return; + default: + throw SelfCheck.Failure($"Internal error: unknown route resolution result {result}."); + } + } + + /// + /// Associates a HTTP method and a route to a handler. + /// + /// A constant representing the HTTP method + /// to associate with , or + /// if can handle all HTTP methods. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void AddHandler(HttpVerbs verb, RouteMatcher matcher, RouteHandlerCallback handler) + => _resolvers.Add(verb, matcher, handler); + + /// + /// Associates a HTTP method and a route to a synchronous handler. + /// + /// A constant representing the HTTP method + /// to associate with , or + /// if can handle all HTTP methods. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void AddHandler(HttpVerbs verb, RouteMatcher matcher, SyncRouteHandlerCallback handler) + => _resolvers.Add(verb, matcher, handler); + + /// + /// Adds handlers, associating them with HTTP method / route pairs by means + /// of Route attributes. + /// See for further information. + /// + /// Where to look for compatible handlers. + /// The number of handlers that were added. + /// is . + protected int AddHandlersFrom(object target) + => _resolvers.AddFrom(target); + + /// + /// Associates all requests matching a route to a handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnAny(RouteMatcher matcher, RouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Any, matcher, handler); + + /// + /// Associates all requests matching a route to a synchronous handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnAny(RouteMatcher matcher, SyncRouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Any, matcher, handler); + + /// + /// Associates DELETE requests matching a route to a handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnDelete(RouteMatcher matcher, RouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Delete, matcher, handler); + + /// + /// Associates DELETE requests matching a route to a synchronous handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnDelete(RouteMatcher matcher, SyncRouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Delete, matcher, handler); + + /// + /// Associates GET requests matching a route to a handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnGet(RouteMatcher matcher, RouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Get, matcher, handler); + + /// + /// Associates GET requests matching a route to a synchronous handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnGet(RouteMatcher matcher, SyncRouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Get, matcher, handler); + + /// + /// Associates HEAD requests matching a route to a handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnHead(RouteMatcher matcher, RouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Head, matcher, handler); + + /// + /// Associates HEAD requests matching a route to a synchronous handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnHead(RouteMatcher matcher, SyncRouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Head, matcher, handler); + + /// + /// Associates OPTIONS requests matching a route to a handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnOptions(RouteMatcher matcher, RouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Options, matcher, handler); + + /// + /// Associates OPTIONS requests matching a route to a synchronous handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnOptions(RouteMatcher matcher, SyncRouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Options, matcher, handler); + + /// + /// Associates PATCH requests matching a route to a handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnPatch(RouteMatcher matcher, RouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Patch, matcher, handler); + + /// + /// Associates PATCH requests matching a route to a synchronous handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnPatch(RouteMatcher matcher, SyncRouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Patch, matcher, handler); + + /// + /// Associates POST requests matching a route to a handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnPost(RouteMatcher matcher, RouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Post, matcher, handler); + + /// + /// Associates POST requests matching a route to a synchronous handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnPost(RouteMatcher matcher, SyncRouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Post, matcher, handler); + + /// + /// Associates PUT requests matching a route to a handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnPut(RouteMatcher matcher, RouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Put, matcher, handler); + + /// + /// Associates PUT requests matching a route to a synchronous handler. + /// + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// + /// is . + /// - or - + /// is . + /// + protected void OnPut(RouteMatcher matcher, SyncRouteHandlerCallback handler) + => _resolvers.Add(HttpVerbs.Put, matcher, handler); + + /// + /// Called when no route is matched for the requested URL path. + /// The default behavior is to send an empty 404 Not Found response. + /// + /// The context of the request being handled. + /// A representing the ongoing operation. + protected virtual Task OnPathNotFoundAsync(IHttpContext context) + => throw HttpException.NotFound(); + + /// + /// Called when at least one route is matched for the requested URL path, + /// but none of them is associated with the HTTP method of the request. + /// The default behavior is to send an empty 405 Method Not Allowed response. + /// + /// The context of the request being handled. + /// A representing the ongoing operation. + protected virtual Task OnMethodNotAllowedAsync(IHttpContext context) + => throw HttpException.MethodNotAllowed(); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/RoutingModuleExtensions-AddHandlerFromBaseOrTerminalRoute.cs b/Vendor/EmbedIO-3.5.2/Routing/RoutingModuleExtensions-AddHandlerFromBaseOrTerminalRoute.cs new file mode 100644 index 0000000..321a738 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/RoutingModuleExtensions-AddHandlerFromBaseOrTerminalRoute.cs @@ -0,0 +1,429 @@ +using System; + +namespace EmbedIO.Routing +{ + partial class RoutingModuleExtensions + { + /// + /// Adds a handler to a . + /// + /// The on which this method is called. + /// A constant representing the HTTP method + /// to associate with , or + /// if can handle all HTTP methods. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + /// + public static RoutingModule Handle(this RoutingModule @this, HttpVerbs verb, string route, bool isBaseRoute, RouteHandlerCallback handler) + { + @this.Add(verb, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Adds a synchronous handler to a . + /// + /// The on which this method is called. + /// A constant representing the HTTP method + /// to associate with , or + /// if can handle all HTTP methods. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + /// + public static RoutingModule Handle(this RoutingModule @this, HttpVerbs verb, string route, bool isBaseRoute, SyncRouteHandlerCallback handler) + { + @this.Add(verb, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates all requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnAny(this RoutingModule @this, string route, bool isBaseRoute, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Any, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates all requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnAny(this RoutingModule @this, string route, bool isBaseRoute, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Any, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates DELETE requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnDelete(this RoutingModule @this, string route, bool isBaseRoute, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Delete, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates DELETE requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnDelete(this RoutingModule @this, string route, bool isBaseRoute, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Delete, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates GET requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnGet(this RoutingModule @this, string route, bool isBaseRoute, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Get, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates GET requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnGet(this RoutingModule @this, string route, bool isBaseRoute, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Get, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates HEAD requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnHead(this RoutingModule @this, string route, bool isBaseRoute, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Head, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates HEAD requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnHead(this RoutingModule @this, string route, bool isBaseRoute, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Head, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates OPTIONS requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnOptions(this RoutingModule @this, string route, bool isBaseRoute, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Options, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates OPTIONS requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnOptions(this RoutingModule @this, string route, bool isBaseRoute, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Options, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates PATCH requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnPatch(this RoutingModule @this, string route, bool isBaseRoute, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Patch, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates PATCH requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnPatch(this RoutingModule @this, string route, bool isBaseRoute, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Patch, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates POST requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnPost(this RoutingModule @this, string route, bool isBaseRoute, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Post, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates POST requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnPost(this RoutingModule @this, string route, bool isBaseRoute, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Post, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates PUT requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnPut(this RoutingModule @this, string route, bool isBaseRoute, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Put, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + + /// + /// Associates PUT requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// if + /// is a base route; if + /// is a terminal (non-base) route. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnPut(this RoutingModule @this, string route, bool isBaseRoute, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Put, RouteMatcher.Parse(route, isBaseRoute), handler); + return @this; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/RoutingModuleExtensions-AddHandlerFromRouteMatcher.cs b/Vendor/EmbedIO-3.5.2/Routing/RoutingModuleExtensions-AddHandlerFromRouteMatcher.cs new file mode 100644 index 0000000..fb0e255 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/RoutingModuleExtensions-AddHandlerFromRouteMatcher.cs @@ -0,0 +1,357 @@ +using System; + +namespace EmbedIO.Routing +{ + partial class RoutingModuleExtensions + { + /// + /// Adds a handler to a . + /// + /// The on which this method is called. + /// A constant representing the HTTP method + /// to associate with , or + /// if can handle all HTTP methods. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// + public static RoutingModule Handle(this RoutingModule @this, HttpVerbs verb, RouteMatcher matcher, RouteHandlerCallback handler) + { + @this.Add(verb, matcher, handler); + return @this; + } + + /// + /// Adds a synchronous handler to a . + /// + /// The on which this method is called. + /// A constant representing the HTTP method + /// to associate with , or + /// if can handle all HTTP methods. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// + public static RoutingModule Handle(this RoutingModule @this, HttpVerbs verb, RouteMatcher matcher, SyncRouteHandlerCallback handler) + { + @this.Add(verb, matcher, handler); + return @this; + } + + /// + /// Associates all requests matching a route to a handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnAny(this RoutingModule @this, RouteMatcher matcher, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Any, matcher, handler); + return @this; + } + + /// + /// Associates all requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnAny(this RoutingModule @this, RouteMatcher matcher, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Any, matcher, handler); + return @this; + } + + /// + /// Associates DELETE requests matching a route to a handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnDelete(this RoutingModule @this, RouteMatcher matcher, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Delete, matcher, handler); + return @this; + } + + /// + /// Associates DELETE requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnDelete(this RoutingModule @this, RouteMatcher matcher, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Delete, matcher, handler); + return @this; + } + + /// + /// Associates GET requests matching a route to a handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnGet(this RoutingModule @this, RouteMatcher matcher, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Get, matcher, handler); + return @this; + } + + /// + /// Associates GET requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnGet(this RoutingModule @this, RouteMatcher matcher, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Get, matcher, handler); + return @this; + } + + /// + /// Associates HEAD requests matching a route to a handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnHead(this RoutingModule @this, RouteMatcher matcher, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Head, matcher, handler); + return @this; + } + + /// + /// Associates HEAD requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnHead(this RoutingModule @this, RouteMatcher matcher, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Head, matcher, handler); + return @this; + } + + /// + /// Associates OPTIONS requests matching a route to a handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnOptions(this RoutingModule @this, RouteMatcher matcher, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Options, matcher, handler); + return @this; + } + + /// + /// Associates OPTIONS requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnOptions(this RoutingModule @this, RouteMatcher matcher, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Options, matcher, handler); + return @this; + } + + /// + /// Associates PATCH requests matching a route to a handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnPatch(this RoutingModule @this, RouteMatcher matcher, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Patch, matcher, handler); + return @this; + } + + /// + /// Associates PATCH requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnPatch(this RoutingModule @this, RouteMatcher matcher, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Patch, matcher, handler); + return @this; + } + + /// + /// Associates POST requests matching a route to a handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnPost(this RoutingModule @this, RouteMatcher matcher, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Post, matcher, handler); + return @this; + } + + /// + /// Associates POST requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnPost(this RoutingModule @this, RouteMatcher matcher, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Post, matcher, handler); + return @this; + } + + /// + /// Associates PUT requests matching a route to a handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnPut(this RoutingModule @this, RouteMatcher matcher, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Put, matcher, handler); + return @this; + } + + /// + /// Associates PUT requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The used to match URL paths. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + public static RoutingModule OnPut(this RoutingModule @this, RouteMatcher matcher, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Put, matcher, handler); + return @this; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/RoutingModuleExtensions-AddHandlerFromTerminalRoute.cs b/Vendor/EmbedIO-3.5.2/Routing/RoutingModuleExtensions-AddHandlerFromTerminalRoute.cs new file mode 100644 index 0000000..4e34ae7 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/RoutingModuleExtensions-AddHandlerFromTerminalRoute.cs @@ -0,0 +1,375 @@ +using System; + +namespace EmbedIO.Routing +{ + partial class RoutingModuleExtensions + { + /// + /// Adds a handler to a . + /// + /// The on which this method is called. + /// A constant representing the HTTP method + /// to associate with , or + /// if can handle all HTTP methods. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + /// + public static RoutingModule Handle(this RoutingModule @this, HttpVerbs verb, string route, RouteHandlerCallback handler) + { + @this.Add(verb, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Adds a synchronous handler to a . + /// + /// The on which this method is called. + /// A constant representing the HTTP method + /// to associate with , or + /// if can handle all HTTP methods. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + /// + public static RoutingModule Handle(this RoutingModule @this, HttpVerbs verb, string route, SyncRouteHandlerCallback handler) + { + @this.Add(verb, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates all requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnAny(this RoutingModule @this, string route, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Any, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates all requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnAny(this RoutingModule @this, string route, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Any, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates DELETE requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnDelete(this RoutingModule @this, string route, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Delete, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates DELETE requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnDelete(this RoutingModule @this, string route, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Delete, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates GET requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnGet(this RoutingModule @this, string route, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Get, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates GET requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnGet(this RoutingModule @this, string route, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Get, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates HEAD requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnHead(this RoutingModule @this, string route, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Head, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates HEAD requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnHead(this RoutingModule @this, string route, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Head, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates OPTIONS requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnOptions(this RoutingModule @this, string route, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Options, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates OPTIONS requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnOptions(this RoutingModule @this, string route, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Options, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates PATCH requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnPatch(this RoutingModule @this, string route, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Patch, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates PATCH requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnPatch(this RoutingModule @this, string route, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Patch, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates POST requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnPost(this RoutingModule @this, string route, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Post, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates POST requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnPost(this RoutingModule @this, string route, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Post, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates PUT requests matching a route to a handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnPut(this RoutingModule @this, string route, RouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Put, RouteMatcher.Parse(route, false), handler); + return @this; + } + + /// + /// Associates PUT requests matching a route to a synchronous handler. + /// + /// The on which this method is called. + /// The route to match URL paths against. + /// A callback used to handle matching contexts. + /// with the handler added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// is not a valid route. + public static RoutingModule OnPut(this RoutingModule @this, string route, SyncRouteHandlerCallback handler) + { + @this.Add(HttpVerbs.Put, RouteMatcher.Parse(route, false), handler); + return @this; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/RoutingModuleExtensions.cs b/Vendor/EmbedIO-3.5.2/Routing/RoutingModuleExtensions.cs new file mode 100644 index 0000000..8be3263 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/RoutingModuleExtensions.cs @@ -0,0 +1,26 @@ +using System; + +namespace EmbedIO.Routing +{ + /// + /// Provides extension methods for . + /// + public static partial class RoutingModuleExtensions + { + /// + /// Adds handlers, associating them with HTTP method / route pairs by means + /// of Route attributes. + /// See for further information. + /// + /// The on which this method is called. + /// Where to look for compatible handlers. + /// with handlers added. + /// is . + /// is . + public static RoutingModule WithHandlersFrom(this RoutingModule @this, object target) + { + @this.AddFrom(target); + return @this; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Routing/SyncRouteHandlerCallback.cs b/Vendor/EmbedIO-3.5.2/Routing/SyncRouteHandlerCallback.cs new file mode 100644 index 0000000..c858b9f --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Routing/SyncRouteHandlerCallback.cs @@ -0,0 +1,10 @@ +namespace EmbedIO.Routing +{ + /// + /// Base class for callbacks used to handle routed requests synchronously. + /// + /// An interface representing the context of the request. + /// The matched route. + /// + public delegate void SyncRouteHandlerCallback(IHttpContext context, RouteMatch route); +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Security/BanInfo.cs b/Vendor/EmbedIO-3.5.2/Security/BanInfo.cs new file mode 100644 index 0000000..83a6269 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Security/BanInfo.cs @@ -0,0 +1,25 @@ +using System.Net; + +namespace EmbedIO.Security +{ + /// + /// Contains information about the ban of an IP address. + /// + public class BanInfo + { + /// + /// Gets or sets the banned IP address. + /// + public IPAddress IPAddress { get; set; } + + /// + /// Gets or sets the expiration time of the ban. + /// + public long ExpiresAt { get; set; } + + /// + /// Gets or sets a value indicating whether this instance was explicitly banned. + /// + public bool IsExplicit { get; set; } + } +} diff --git a/Vendor/EmbedIO-3.5.2/Security/IIPBanningCriterion.cs b/Vendor/EmbedIO-3.5.2/Security/IIPBanningCriterion.cs new file mode 100644 index 0000000..7a13ee1 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Security/IIPBanningCriterion.cs @@ -0,0 +1,30 @@ +using System; +using System.Net; +using System.Threading.Tasks; + +namespace EmbedIO.Security +{ + /// + /// Represents a criterion for . + /// + public interface IIPBanningCriterion : IDisposable + { + /// + /// Validates the IP address should be banned or not. + /// + /// The address. + /// true if the IP Address should be banned, otherwise false. + Task ValidateIPAddress(IPAddress address); + + /// + /// Clears the data generated by an IP address. + /// + /// The address. + void ClearIPAddress(IPAddress address); + + /// + /// Purges the data of the Criterion. + /// + void PurgeData(); + } +} diff --git a/Vendor/EmbedIO-3.5.2/Security/IPBanningConfiguration.cs b/Vendor/EmbedIO-3.5.2/Security/IPBanningConfiguration.cs new file mode 100644 index 0000000..a91b89f --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Security/IPBanningConfiguration.cs @@ -0,0 +1,188 @@ +using EmbedIO.Utilities; +using Swan.Configuration; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +namespace EmbedIO.Security +{ + /// + /// Represents a configuration object for . + /// + /// + public class IPBanningConfiguration : ConfiguredObject, IDisposable + { + private readonly List _criterions = new List(); + private readonly ConcurrentDictionary _blacklistDictionary = new ConcurrentDictionary(); + private readonly ConcurrentBag _whiteListBag = new ConcurrentBag(); + private readonly int _banTime; + private bool _disposed; + + internal IPBanningConfiguration(int banTime) + { + _banTime = banTime; + } + + /// + /// Finalizes an instance of the class. + /// + ~IPBanningConfiguration() + { + Dispose(false); + } + + /// + /// Gets the black list. + /// + /// + /// The black list. + /// + public List BlackList => _blacklistDictionary.Values.ToList(); + + /// + /// Check if a Criterion should continue testing an IP Address. + /// + /// The address. + /// true if the Criterion should continue, otherwise false. + public bool ShouldContinue(IPAddress address) => + !_whiteListBag.Contains(address) || !_blacklistDictionary.ContainsKey(address); + + /// + /// Purges this instance. + /// + public void Purge() + { + PurgeBlackList(); + + foreach (var criterion in _criterions) + { + criterion.PurgeData(); + } + } + + /// + /// Checks the client. + /// + /// The client address. + /// A representing the asynchronous operation. + public async Task CheckClient(IPAddress clientAddress) + { + if (_whiteListBag.Contains(clientAddress)) + return; + + foreach (var criterion in _criterions) + { + var result = await criterion.ValidateIPAddress(clientAddress).ConfigureAwait(false); + + if (!result) continue; + + TryBanIP(clientAddress, false); + break; + } + + if (_blacklistDictionary.ContainsKey(clientAddress)) + throw HttpException.Forbidden(); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + internal async Task AddToWhitelistAsync(IEnumerable? whitelist) + { + if (whitelist?.Any() != true) + return; + + foreach (var whiteAddress in whitelist) + { + var parsedAddresses = await IPParser.ParseAsync(whiteAddress).ConfigureAwait(false); + foreach (var address in parsedAddresses.Where(x => !_whiteListBag.Contains(x))) + { + _whiteListBag.Add(address); + } + } + } + + internal void Lock() => LockConfiguration(); + + internal bool TryRemoveBlackList(IPAddress address) + { + foreach (var criterion in _criterions) + { + criterion.ClearIPAddress(address); + } + + return _blacklistDictionary.TryRemove(address, out _); + } + + internal void RegisterCriterion(IIPBanningCriterion criterion) + { + EnsureConfigurationNotLocked(); + _criterions.Add(criterion); + } + + internal bool TryBanIP(IPAddress address, bool isExplicit, DateTime? banUntil = null) + { + try + { + _blacklistDictionary.AddOrUpdate(address, + k => + new BanInfo + { + IPAddress = k, + ExpiresAt = banUntil?.Ticks ?? DateTime.Now.AddMinutes(_banTime).Ticks, + IsExplicit = isExplicit, + }, + (k, v) => + new BanInfo + { + IPAddress = k, + ExpiresAt = banUntil?.Ticks ?? DateTime.Now.AddMinutes(_banTime).Ticks, + IsExplicit = isExplicit, + }); + + return true; + } + catch + { + return false; + } + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + _blacklistDictionary.Clear(); + + _criterions.ForEach(x => x.Dispose()); + _criterions.Clear(); + } + + _disposed = true; + } + + private void PurgeBlackList() + { + foreach (var k in _blacklistDictionary.Keys) + { + if (_blacklistDictionary.TryGetValue(k, out var info) && + DateTime.Now.Ticks > info.ExpiresAt) + _blacklistDictionary.TryRemove(k, out _); + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Security/IPBanningModule.cs b/Vendor/EmbedIO-3.5.2/Security/IPBanningModule.cs new file mode 100644 index 0000000..639b33c --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Security/IPBanningModule.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Security.Internal; + +namespace EmbedIO.Security +{ + /// + /// A module to ban clients by IP address, based on TCP requests-per-second or RegEx matches on log messages. + /// + /// + public class IPBanningModule : WebModuleBase, IDisposable + { + /// + /// The default ban minutes. + /// + public const int DefaultBanMinutes = 30; + + private const string NoConfigurationFound = "No configuration was found for the base route provided."; + + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The base route. + /// A collection of valid IPs that never will be banned. + /// Minutes that an IP will remain banned. + public IPBanningModule(string baseRoute = "/", + IEnumerable? whitelist = null, + int banMinutes = DefaultBanMinutes) + : base(baseRoute) + { + Configuration = IPBanningExecutor.RetrieveInstance(baseRoute, banMinutes); + + AddToWhitelist(whitelist); + } + + /// + /// Finalizes an instance of the class. + /// + ~IPBanningModule() + { + Dispose(false); + } + + /// + public override bool IsFinalHandler => false; + + /// + /// Gets the client address. + /// + /// + /// The client address. + /// + public IPAddress? ClientAddress { get; private set; } + + internal IPBanningConfiguration Configuration { get; } + + /// + /// Registers the criterion. + /// + /// The criterion. + public void RegisterCriterion(IIPBanningCriterion criterion) => + Configuration.RegisterCriterion(criterion); + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Gets the list of current banned IPs. + /// + /// The base route. + /// + /// A collection of in the blacklist. + /// + /// baseRoute + public static IEnumerable GetBannedIPs(string baseRoute = "/") => + IPBanningExecutor.TryGetInstance(baseRoute, out var instance) + ? instance.BlackList + : throw new ArgumentException(NoConfigurationFound, nameof(baseRoute)); + + /// + /// Tries to ban an IP explicitly. + /// + /// The IP address to ban. + /// Minutes that the IP will remain banned. + /// The base route. + /// true if the IP was explicitly banned. + /// + /// true if the IP was added to the blacklist; otherwise, false. + /// + public static bool TryBanIP(IPAddress address, int banMinutes, string baseRoute = "/", bool isExplicit = true) => + TryBanIP(address, DateTime.Now.AddMinutes(banMinutes), baseRoute, isExplicit); + + /// + /// Tries to ban an IP explicitly. + /// + /// The IP address to ban. + /// A specifying the duration that the IP will remain banned. + /// The base route. + /// true if the IP was explicitly banned. + /// + /// true if the IP was added to the blacklist; otherwise, false. + /// + public static bool TryBanIP(IPAddress address, TimeSpan banDuration, string baseRoute = "/", bool isExplicit = true) => + TryBanIP(address, DateTime.Now.Add(banDuration), baseRoute, isExplicit); + + /// + /// Tries to ban an IP explicitly. + /// + /// The IP address to ban. + /// A specifying the expiration time of the ban. + /// The base route. + /// true if the IP was explicitly banned. + /// + /// true if the IP was added to the blacklist; otherwise, false. + /// + /// baseRoute + public static bool TryBanIP(IPAddress address, DateTime banUntil, string baseRoute = "/", bool isExplicit = true) + { + if (!IPBanningExecutor.TryGetInstance(baseRoute, out var instance)) + throw new ArgumentException(NoConfigurationFound, nameof(baseRoute)); + + return instance.TryBanIP(address, isExplicit, banUntil); + } + + /// + /// Tries to unban an IP explicitly. + /// + /// The IP address. + /// The base route. + /// + /// true if the IP was removed from the blacklist; otherwise, false. + /// + /// baseRoute + public static bool TryUnbanIP(IPAddress address, string baseRoute = "/") => + IPBanningExecutor.TryGetInstance(baseRoute, out var instance) + ? instance.TryRemoveBlackList(address) + : throw new ArgumentException(NoConfigurationFound, nameof(baseRoute)); + + internal void AddToWhitelist(IEnumerable? whitelist) => + Configuration.AddToWhitelistAsync(whitelist).GetAwaiter().GetResult(); + + /// + protected override void OnStart(CancellationToken cancellationToken) + { + Configuration.Lock(); + + base.OnStart(cancellationToken); + } + + /// + protected override Task OnRequestAsync(IHttpContext context) + { + ClientAddress = context.Request.RemoteEndPoint.Address; + return Configuration.CheckClient(ClientAddress); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_disposed) return; + if (disposing) + { + IPBanningExecutor.TryRemoveInstance(BaseRoute); + Configuration.Dispose(); + } + + _disposed = true; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Security/IPBanningModuleExtensions.cs b/Vendor/EmbedIO-3.5.2/Security/IPBanningModuleExtensions.cs new file mode 100644 index 0000000..fd0d9a0 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Security/IPBanningModuleExtensions.cs @@ -0,0 +1,74 @@ +namespace EmbedIO.Security +{ + /// + /// Provides extension methods for and derived classes. + /// + public static class IPBanningModuleExtensions + { + /// + /// Adds a collection of valid IPs that never will be banned. + /// + /// The type of the module. + /// The module on which this method is called. + /// A collection of valid IPs that never will be banned. + /// + /// with its whitelist configured. + /// + public static TModule WithWhitelist(this TModule @this, params string[] value) + where TModule : IPBanningModule + { + @this.AddToWhitelist(value); + return @this; + } + + /// + /// Add a collection of Regex to match the log messages against as a criterion for banning IP addresses. + /// + /// The type of the module. + /// The module on which this method is called. + /// A collection of regex to match log messages against. + /// + /// with a fail regex criterion configured. + /// + public static TModule WithRegexRules(this TModule @this, params string[] value) + where TModule : IPBanningModule => + WithRegexRules(@this, IPBanningRegexCriterion.DefaultMaxMatchCount, IPBanningRegexCriterion.DefaultSecondsMatchingPeriod, value); + + /// + /// Add a collection of Regex to match the log messages against as a criterion for banning IP addresses. + /// + /// The type of the module. + /// The module on which this method is called. + /// The maximum match count. + /// The seconds matching period. + /// A collection of regex to match log messages against. + /// + /// with a fail regex criterion configured. + /// + public static TModule WithRegexRules(this TModule @this, + int maxMatchCount, + int secondsMatchingPeriod, + params string[] value) + where TModule : IPBanningModule + { + @this.RegisterCriterion(new IPBanningRegexCriterion(@this, value, maxMatchCount, secondsMatchingPeriod)); + return @this; + } + + /// + /// Sets a maximum amount of requests per second as a criterion for banning IP addresses. + /// + /// The type of the module. + /// The module on which this method is called. + /// The maximum requests per second. + /// + /// with a maximum requests per second configured. + /// + public static TModule WithMaxRequestsPerSecond(this TModule @this, int maxRequests = IPBanningRequestsCriterion.DefaultMaxRequestsPerSecond) + where TModule : IPBanningModule + { + @this.RegisterCriterion(new IPBanningRequestsCriterion(maxRequests)); + return @this; + } + } +} diff --git a/Vendor/EmbedIO-3.5.2/Security/IPBanningRegexCriterion.cs b/Vendor/EmbedIO-3.5.2/Security/IPBanningRegexCriterion.cs new file mode 100644 index 0000000..810d1bb --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Security/IPBanningRegexCriterion.cs @@ -0,0 +1,197 @@ +using Swan.Logging; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace EmbedIO.Security +{ + /// + /// Represents a log message regex matching criterion for . + /// + /// + public class IPBanningRegexCriterion : IIPBanningCriterion + { + /// + /// The default matching period. + /// + public const int DefaultSecondsMatchingPeriod = 60; + + /// + /// The default maximum match count per period. + /// + public const int DefaultMaxMatchCount = 10; + + private readonly ConcurrentDictionary> _failRegexMatches = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary _failRegex = new ConcurrentDictionary(); + private readonly IPBanningModule _parent; + private readonly int _secondsMatchingPeriod; + private readonly int _maxMatchCount; + private readonly ILogger? _innerLogger; + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The parent. + /// The rules. + /// The maximum match count. + /// The seconds matching period. + public IPBanningRegexCriterion(IPBanningModule parent, IEnumerable rules, int maxMatchCount = DefaultMaxMatchCount, int secondsMatchingPeriod = DefaultSecondsMatchingPeriod) + { + _secondsMatchingPeriod = secondsMatchingPeriod; + _maxMatchCount = maxMatchCount; + _parent = parent; + + AddRules(rules); + + if (_failRegex.Any()) + _innerLogger = new InnerRegexCriterionLogger(this); + } + + /// + /// Finalizes an instance of the class. + /// + ~IPBanningRegexCriterion() + { + Dispose(false); + } + + /// + public Task ValidateIPAddress(IPAddress address) + { + var minTime = DateTime.Now.AddSeconds(-1 * _secondsMatchingPeriod).Ticks; + var shouldBan = _failRegexMatches.TryGetValue(address, out var attempts) && + attempts.Count(x => x >= minTime) >= _maxMatchCount; + + return Task.FromResult(shouldBan); + } + + /// + public void ClearIPAddress(IPAddress address) => + _failRegexMatches.TryRemove(address, out _); + + /// + public void PurgeData() + { + var minTime = DateTime.Now.AddSeconds(-1 * _secondsMatchingPeriod).Ticks; + + foreach (var k in _failRegexMatches.Keys) + { + if (!_failRegexMatches.TryGetValue(k, out var failRegexMatches)) continue; + + var recentMatches = new ConcurrentBag(failRegexMatches.Where(x => x >= minTime)); + if (!recentMatches.Any()) + _failRegexMatches.TryRemove(k, out _); + else + _failRegexMatches.AddOrUpdate(k, recentMatches, (x, y) => recentMatches); + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + _failRegexMatches.Clear(); + _failRegex.Clear(); + if (_innerLogger != null) + { + try + { + Logger.UnregisterLogger(_innerLogger); + } + catch + { + // ignore + } + + _innerLogger.Dispose(); + } + } + + _disposed = true; + } + + private void MatchIP(IPAddress address, string message) + { + if (!_parent.Configuration.ShouldContinue(address)) + return; + + foreach (var regex in _failRegex.Values) + { + try + { + if (!regex.IsMatch(message)) continue; + + _failRegexMatches.GetOrAdd(address, new ConcurrentBag()).Add(DateTime.Now.Ticks); + break; + } + catch (RegexMatchTimeoutException ex) + { + $"Timeout trying to match '{ex.Input}' with pattern '{ex.Pattern}'.".Error(nameof(InnerRegexCriterionLogger)); + } + } + } + + private void AddRules(IEnumerable patterns) + { + foreach (var pattern in patterns) + AddRule(pattern); + } + + private void AddRule(string pattern) + { + try + { + _failRegex.TryAdd(pattern, new Regex(pattern, RegexOptions.Compiled | RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(500))); + } + catch (Exception ex) + { + ex.Log(nameof(IPBanningModule), $"Invalid regex - '{pattern}'."); + } + } + + private sealed class InnerRegexCriterionLogger : ILogger + { + private readonly IPBanningRegexCriterion _parent; + + public InnerRegexCriterionLogger(IPBanningRegexCriterion parent) + { + _parent = parent; + Logger.RegisterLogger(this); + } + + /// + public LogLevel LogLevel => LogLevel.Trace; + + public void Dispose() + { + // DO nothing + } + + /// + public void Log(LogMessageReceivedEventArgs logEvent) + { + var clientAddress = _parent._parent.ClientAddress; + + if (clientAddress == null || string.IsNullOrWhiteSpace(logEvent.Message)) + return; + + _parent.MatchIP(clientAddress, logEvent.Message); + } + } + } +} diff --git a/Vendor/EmbedIO-3.5.2/Security/IPBanningRequestsCriterion.cs b/Vendor/EmbedIO-3.5.2/Security/IPBanningRequestsCriterion.cs new file mode 100644 index 0000000..0cb2294 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Security/IPBanningRequestsCriterion.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +namespace EmbedIO.Security +{ + /// + /// Represents a maximun requests per second criterion for . + /// + /// + public class IPBanningRequestsCriterion : IIPBanningCriterion + { + /// + /// The default maximum request per second. + /// + public const int DefaultMaxRequestsPerSecond = 50; + + private static readonly ConcurrentDictionary> Requests = new ConcurrentDictionary>(); + + private readonly int _maxRequestsPerSecond; + + private bool _disposed; + + internal IPBanningRequestsCriterion(int maxRequestsPerSecond) + { + _maxRequestsPerSecond = maxRequestsPerSecond; + } + + /// + /// Finalizes an instance of the class. + /// + ~IPBanningRequestsCriterion() + { + Dispose(false); + } + + /// + public Task ValidateIPAddress(IPAddress address) + { + Requests.GetOrAdd(address, new ConcurrentBag()).Add(DateTime.Now.Ticks); + + var lastSecond = DateTime.Now.AddSeconds(-1).Ticks; + var lastMinute = DateTime.Now.AddMinutes(-1).Ticks; + + var shouldBan = Requests.TryGetValue(address, out var attempts) && + (attempts.Count(x => x >= lastSecond) >= _maxRequestsPerSecond || + (attempts.Count(x => x >= lastMinute) / 60) >= _maxRequestsPerSecond); + + return Task.FromResult(shouldBan); + } + + /// + public void ClearIPAddress(IPAddress address) => + Requests.TryRemove(address, out _); + + /// + public void PurgeData() + { + var minTime = DateTime.Now.AddMinutes(-1).Ticks; + + foreach (var k in Requests.Keys) + { + if (!Requests.TryGetValue(k, out var requests)) continue; + + var recentRequests = new ConcurrentBag(requests.Where(x => x >= minTime)); + if (!recentRequests.Any()) + Requests.TryRemove(k, out _); + else + Requests.AddOrUpdate(k, recentRequests, (x, y) => recentRequests); + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) return; + if (disposing) + { + Requests.Clear(); + } + + _disposed = true; + } + } +} diff --git a/Vendor/EmbedIO-3.5.2/Security/Internal/IPBanningExecutor.cs b/Vendor/EmbedIO-3.5.2/Security/Internal/IPBanningExecutor.cs new file mode 100644 index 0000000..b20dad9 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Security/Internal/IPBanningExecutor.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using Swan.Threading; + +namespace EmbedIO.Security.Internal +{ + internal static class IPBanningExecutor + { + private static readonly ConcurrentDictionary Configurations = new ConcurrentDictionary(); + + private static readonly PeriodicTask Purger = new PeriodicTask(TimeSpan.FromMinutes(1), ct => { + foreach (var conf in Configurations.Keys) + { + if (Configurations.TryGetValue(conf, out var instance)) + instance.Purge(); + } + + return Task.CompletedTask; + }); + + public static IPBanningConfiguration RetrieveInstance(string baseRoute, int banMinutes) => + Configurations.GetOrAdd(baseRoute, x => new IPBanningConfiguration(banMinutes)); + + public static bool TryGetInstance(string baseRoute, out IPBanningConfiguration configuration) => + Configurations.TryGetValue(baseRoute, out configuration); + + public static bool TryRemoveInstance(string baseRoute) => + Configurations.TryRemove(baseRoute, out _); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Sessions/ISession.cs b/Vendor/EmbedIO-3.5.2/Sessions/ISession.cs new file mode 100644 index 0000000..5222687 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Sessions/ISession.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; + +namespace EmbedIO.Sessions +{ + /// + /// Represents a session. + /// + public interface ISession + { + /// + /// A unique identifier for the session. + /// + /// The unique identifier for this session. + /// + /// + string Id { get; } + + /// + /// Gets the time interval, starting from , + /// after which the session expires. + /// + /// The expiration time. + TimeSpan Duration { get; } + + /// + /// Gets the UTC date and time of last activity on the session. + /// + /// + /// The UTC date and time of last activity on the session. + /// + DateTime LastActivity { get; } + + /// + /// Gets the number of key/value pairs contained in a session. + /// + /// + /// The number of key/value pairs contained in the object that implements . + /// + int Count { get; } + + /// + /// Gets a value that indicates whether a session is empty. + /// + /// + /// if the object that implements is empty, + /// i.e. contains no key / value pairs; otherwise, . + /// + bool IsEmpty { get; } + + /// + /// Gets or sets the value associated with the specified key. + /// Note that a session does not store null values; therefore, setting this property to + /// has the same effect as removing from the dictionary. + /// + /// + /// The value associated with the specified key, if + /// is found in the dictionary; otherwise, . + /// + /// The key of the value to get or set. + /// is . + object this[string key] { get; set; } + + /// + /// Removes all keys and values from a session. + /// + void Clear(); + + /// + /// Determines whether a session contains an element with the specified key. + /// + /// The key to locate in the object that implements . + /// + /// if the object that implements contains an element with the key; + /// otherwise, . + /// + /// is . + bool ContainsKey(string key); + + /// + /// Gets the value associated with the specified key. + /// + /// The key whose value to get. + /// When this method returns, the value associated with the specified , + /// if the key is found; otherwise, . This parameter is passed uninitialized. + /// if the object that implements + /// contains an element with the specified key; otherwise, . + /// is . + bool TryGetValue(string key, out object value); + + /// + /// Attempts to remove and return the value that has the specified key from a session. + /// + /// The key of the element to remove and return. + /// When this method returns, the value removed from the object that implements , + /// if the key is found; otherwise, . This parameter is passed uninitialized. + /// if the value was removed successfully; otherwise, . + /// is . + bool TryRemove(string key, out object value); + + /// + /// Takes and returns a snapshot of the contents of a session at the time of calling. + /// + /// An IReadOnlyList<KeyValuePair<string,object>> interface + /// containing an immutable copy of the session data as it was at the time of calling this method. + /// + /// The objects contained in the session data are copied by reference, not cloned; therefore + /// you should be aware that their state may change even after the snapshot is taken. + /// + IReadOnlyList> TakeSnapshot(); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Sessions/ISessionManager.cs b/Vendor/EmbedIO-3.5.2/Sessions/ISessionManager.cs new file mode 100644 index 0000000..b82b8ba --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Sessions/ISessionManager.cs @@ -0,0 +1,46 @@ +using System.Threading; + +namespace EmbedIO.Sessions +{ + /// + /// Represents a session manager, which is in charge of managing session objects + /// and their association to HTTP contexts. + /// + public interface ISessionManager + { + /// + /// Signals a session manager that the web server is starting. + /// + /// The cancellation token used to stop the web server. + void Start(CancellationToken cancellationToken); + + /// + /// Returns the session associated with an . + /// If a session ID can be retrieved for the context and stored session data + /// are available, the returned will contain those data; + /// otherwise, a new session is created and its ID is stored in the response + /// to be retrieved by subsequent requests. + /// + /// The HTTP context. + /// An interface. + ISession Create(IHttpContext context); + + /// + /// Deletes the session (if any) associated with the specified context + /// and removes the session's ID from the context. + /// + /// The HTTP context. + /// The unique ID of the session. + /// + void Delete(IHttpContext context, string id); + + /// + /// Called by a session proxy when a session has been obtained + /// for an and the context is closed, + /// even if the session was subsequently deleted. + /// This method can be used to save session data to a storage medium. + /// + /// The for which a session was obtained. + void OnContextClose(IHttpContext context); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Sessions/ISessionProxy.cs b/Vendor/EmbedIO-3.5.2/Sessions/ISessionProxy.cs new file mode 100644 index 0000000..299b8ad --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Sessions/ISessionProxy.cs @@ -0,0 +1,33 @@ +namespace EmbedIO.Sessions +{ + /// + /// Represents a session proxy, i.e. an object that provides + /// the same interface as a session object, plus a basic interface + /// to a session manager. + /// + /// + /// A session proxy can be used just as if it were a session object. + /// A session is automatically created wherever its data are accessed. + /// + /// + public interface ISessionProxy : ISession + { + /// + /// Gets a value indicating whether a session exists for the current context. + /// + /// + /// if a session exists; otherwise, . + /// + bool Exists { get; } + + /// + /// Deletes the session for the current context. + /// + void Delete(); + + /// + /// Deletes the session for the current context and creates a new one. + /// + void Regenerate(); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Sessions/Internal/DummySessionProxy.cs b/Vendor/EmbedIO-3.5.2/Sessions/Internal/DummySessionProxy.cs new file mode 100644 index 0000000..a781a41 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Sessions/Internal/DummySessionProxy.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; + +namespace EmbedIO.Sessions.Internal +{ + internal sealed class DummySessionProxy : ISessionProxy + { + private DummySessionProxy() + { + } + + public static ISessionProxy Instance { get; } = new DummySessionProxy(); + + public bool Exists => false; + + /// + public string Id => throw NoSessionManager(); + + /// + public TimeSpan Duration => throw NoSessionManager(); + + /// + public DateTime LastActivity => throw NoSessionManager(); + + /// + public int Count => 0; + + /// + public bool IsEmpty => true; + + /// + public object this[string key] + { + get => throw NoSessionManager(); + set => throw NoSessionManager(); + } + + /// + public void Delete() + { + } + + /// + public void Regenerate() => throw NoSessionManager(); + + /// + public void Clear() + { + } + + /// + public bool ContainsKey(string key) => throw NoSessionManager(); + + /// + public bool TryGetValue(string key, out object value) => throw NoSessionManager(); + + /// + public bool TryRemove(string key, out object value) => throw NoSessionManager(); + + /// + public IReadOnlyList> TakeSnapshot() => throw NoSessionManager(); + + private InvalidOperationException NoSessionManager() => new InvalidOperationException("No session manager registered in the web server."); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Sessions/LocalSessionManager.SessionImpl.cs b/Vendor/EmbedIO-3.5.2/Sessions/LocalSessionManager.SessionImpl.cs new file mode 100644 index 0000000..4804103 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Sessions/LocalSessionManager.SessionImpl.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using EmbedIO.Utilities; + +namespace EmbedIO.Sessions +{ + partial class LocalSessionManager + { + private class SessionImpl : ISession + { + private readonly Dictionary _data = new Dictionary(Session.KeyComparer); + + private int _usageCount; + + public SessionImpl(string id, TimeSpan duration) + { + Id = Validate.NotNullOrEmpty(nameof(id), id); + Duration = duration; + LastActivity = DateTime.UtcNow; + _usageCount = 1; + } + + public string Id { get; } + + public TimeSpan Duration { get; } + + public DateTime LastActivity { get; private set; } + + public int Count + { + get + { + lock (_data) + { + return _data.Count; + } + } + } + + public bool IsEmpty + { + get + { + lock (_data) + { + return _data.Count == 0; + } + } + } + + public object? this[string key] + { + get + { + lock (_data) + { + return _data.TryGetValue(key, out var value) ? value : null; + } + } + set + { + lock (_data) + { + if (value == null) + _data.Remove(key); + else + _data[key] = value; + } + } + } + + public void Clear() + { + lock (_data) + { + _data.Clear(); + } + } + + public bool ContainsKey(string key) + { + lock (_data) + { + return _data.ContainsKey(key); + } + } + + public bool TryRemove(string key, out object value) + { + lock (_data) + { + if (!_data.TryGetValue(key, out value)) + return false; + + _data.Remove(key); + return true; + } + } + + public IReadOnlyList> TakeSnapshot() + { + lock (_data) + { + return _data.ToArray(); + } + } + + public bool TryGetValue(string key, out object value) + { + lock (_data) + { + return _data.TryGetValue(key, out value); + } + } + + internal void BeginUse() + { + lock (_data) + { + _usageCount++; + LastActivity = DateTime.UtcNow; + } + } + + internal void EndUse(Action unregister) + { + lock (_data) + { + --_usageCount; + UnregisterIfNeededCore(unregister); + } + } + + internal void UnregisterIfNeeded(Action unregister) + { + lock (_data) + { + UnregisterIfNeededCore(unregister); + } + } + + private void UnregisterIfNeededCore(Action unregister) + { + if (_usageCount < 1 && (IsEmpty || DateTime.UtcNow > LastActivity + Duration)) + unregister(); + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Sessions/LocalSessionManager.cs b/Vendor/EmbedIO-3.5.2/Sessions/LocalSessionManager.cs new file mode 100644 index 0000000..54d2ceb --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Sessions/LocalSessionManager.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Utilities; + +namespace EmbedIO.Sessions +{ + /// + /// A simple session manager to handle in-memory sessions. + /// Not for intensive use or for distributed applications. + /// + public partial class LocalSessionManager : ISessionManager + { + /// + /// The default name for session cookies, i.e. "__session". + /// + public const string DefaultCookieName = "__session"; + + /// + /// The default path for session cookies, i.e. "/". + /// + public const string DefaultCookiePath = "/"; + + /// + /// The default HTTP-only flag for session cookies, i.e. . + /// + public const bool DefaultCookieHttpOnly = true; + + /// + /// The default duration for session cookies, i.e. . + /// + public static readonly TimeSpan DefaultCookieDuration = TimeSpan.Zero; + + /// + /// The default duration for sessions, i.e. 30 minutes. + /// + public static readonly TimeSpan DefaultSessionDuration = TimeSpan.FromMinutes(30); + + /// + /// The default interval between automatic purges of expired and empty sessions, i.e. 30 seconds. + /// + public static readonly TimeSpan DefaultPurgeInterval = TimeSpan.FromSeconds(30); + + private readonly ConcurrentDictionary _sessions = + new ConcurrentDictionary(Session.KeyComparer); + + private string _cookieName = DefaultCookieName; + + private string _cookiePath = DefaultCookiePath; + + private TimeSpan _cookieDuration = DefaultCookieDuration; + + private bool _cookieHttpOnly = DefaultCookieHttpOnly; + + private TimeSpan _sessionDuration = DefaultSessionDuration; + + private TimeSpan _purgeInterval = DefaultPurgeInterval; + + /// + /// Initializes a new instance of the class + /// with default values for all properties. + /// + /// + /// + /// + /// + /// + /// + public LocalSessionManager() + { + } + + /// + /// Gets or sets the duration of newly-created sessions. + /// + /// This property is being set after calling + /// the method. + /// + public TimeSpan SessionDuration + { + get => _sessionDuration; + set + { + EnsureConfigurationNotLocked(); + _sessionDuration = value; + } + } + + /// + /// Gets or sets the interval between purges of expired sessions. + /// + /// This property is being set after calling + /// the method. + /// + public TimeSpan PurgeInterval + { + get => _purgeInterval; + set + { + EnsureConfigurationNotLocked(); + _purgeInterval = value; + } + } + + /// + /// Gets or sets the name for session cookies. + /// + /// This property is being set after calling + /// the method. + /// This property is being set to . + /// This property is being set and the provided value + /// is not a valid URL path. + /// + public string CookieName + { + get => _cookieName; + set + { + EnsureConfigurationNotLocked(); + _cookieName = Validate.Rfc2616Token(nameof(value), value); + } + } + + /// + /// Gets or sets the path for session cookies. + /// + /// This property is being set after calling + /// the method. + /// This property is being set to . + /// This property is being set and the provided value + /// is not a valid URL path. + /// + public string CookiePath + { + get => _cookiePath; + set + { + EnsureConfigurationNotLocked(); + _cookiePath = Validate.UrlPath(nameof(value), value, true); + } + } + + /// + /// Gets or sets the duration of session cookies. + /// + /// This property is being set after calling + /// the method. + /// + public TimeSpan CookieDuration + { + get => _cookieDuration; + set + { + EnsureConfigurationNotLocked(); + _cookieDuration = value; + } + } + + /// + /// Gets or sets a value indicating whether session cookies are hidden from Javascript code running on a user agent. + /// + /// This property is being set after calling + /// the method. + /// + public bool CookieHttpOnly + { + get => _cookieHttpOnly; + set + { + EnsureConfigurationNotLocked(); + _cookieHttpOnly = value; + } + } + + private bool ConfigurationLocked { get; set; } + + /// + public void Start(CancellationToken cancellationToken) + { + ConfigurationLocked = true; + + Task.Run(async () => + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + PurgeExpiredAndEmptySessions(); + await Task.Delay(PurgeInterval, cancellationToken).ConfigureAwait(false); + } + } + catch (TaskCanceledException) + { + // ignore + } + }, cancellationToken); + } + + /// + public ISession Create(IHttpContext context) + { + var id = context.Request.Cookies.FirstOrDefault(IsSessionCookie)?.Value.Trim(); + + SessionImpl session; + lock (_sessions) + { + if (!string.IsNullOrEmpty(id) && _sessions.TryGetValue(id!, out session)) + { + session.BeginUse(); + } + else + { + id = UniqueIdGenerator.GetNext(); + session = new SessionImpl(id, SessionDuration); + _sessions.TryAdd(id, session); + } + } + + context.Request.Cookies.Add(BuildSessionCookie(id)); + context.Response.Cookies.Add(BuildSessionCookie(id)); + return session; + } + + /// + public void Delete(IHttpContext context, string id) + { + lock (_sessions) + { + if (_sessions.TryGetValue(id, out var session)) + session.EndUse(() => _sessions.TryRemove(id, out _)); + } + + context.Request.Cookies.Add(BuildSessionCookie(string.Empty)); + context.Response.Cookies.Add(BuildSessionCookie(string.Empty)); + } + + /// + public void OnContextClose(IHttpContext context) + { + if (!context.Session.Exists) + return; + + var id = context.Session.Id; + lock (_sessions) + { + if (_sessions.TryGetValue(id, out var session)) + { + session.EndUse(() => _sessions.TryRemove(id, out _)); + } + } + } + + private void EnsureConfigurationNotLocked() + { + if (ConfigurationLocked) + throw new InvalidOperationException($"Cannot configure a {nameof(LocalSessionManager)} once it has been started."); + } + + private bool IsSessionCookie(Cookie cookie) + => cookie.Name.Equals(CookieName, StringComparison.OrdinalIgnoreCase) + && !cookie.Expired; + + private Cookie BuildSessionCookie(string? id) + { + var cookie = new Cookie(CookieName, id, CookiePath) + { + HttpOnly = CookieHttpOnly, + }; + + if (CookieDuration > TimeSpan.Zero) + { + cookie.Expires = DateTime.UtcNow.Add(CookieDuration); + } + + return cookie; + } + + private void PurgeExpiredAndEmptySessions() + { + string[] ids; + lock (_sessions) + { + ids = _sessions.Keys.ToArray(); + } + + foreach (var id in ids) + { + lock (_sessions) + { + if (!_sessions.TryGetValue(id, out var session)) + return; + + session.UnregisterIfNeeded(() => _sessions.TryRemove(id, out _)); + } + } + } + + private string GetSessionId(IHttpContext context) => context.Request.Cookies.FirstOrDefault(IsSessionCookie)?.Value.Trim() ?? string.Empty; + } +} diff --git a/Vendor/EmbedIO-3.5.2/Sessions/Session.cs b/Vendor/EmbedIO-3.5.2/Sessions/Session.cs new file mode 100644 index 0000000..bc8d990 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Sessions/Session.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace EmbedIO.Sessions +{ + /// + /// Provides useful constants related to session management. + /// + public static class Session + { + /// + /// The used to disambiguate session IDs. + /// Corresponds to . + /// + public const StringComparison IdComparison = StringComparison.Ordinal; + + /// + /// The used to disambiguate session keys. + /// Corresponds to . + /// + public const StringComparison KeyComparison = StringComparison.InvariantCulture; + + /// + /// The equality comparer used for session IDs. + /// Corresponds to . + /// + public static readonly IEqualityComparer IdComparer = StringComparer.Ordinal; + + /// + /// The equality comparer used for session keys. + /// Corresponds to . + /// + public static readonly IEqualityComparer KeyComparer = StringComparer.InvariantCulture; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Sessions/SessionExtensions.cs b/Vendor/EmbedIO-3.5.2/Sessions/SessionExtensions.cs new file mode 100644 index 0000000..0ac44cc --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Sessions/SessionExtensions.cs @@ -0,0 +1,60 @@ +using System; + +namespace EmbedIO.Sessions +{ + /// + /// Provides extension methods for types implementing . + /// + public static class SessionExtensions + { + /// Gets the value associated with the specified key. + /// The desired type of the value. + /// The on which this method is called. + /// The key whose value to get from the session. + /// + /// When this method returns, the value associated with the specified key, + /// if the key is found and the associated value is of type ; + /// otherwise, the default value for . + /// This parameter is passed uninitialized. + /// + /// if the key is found and the associated value is of type ; + /// otherwise, . + /// is . + /// is . + public static bool TryGetValue(this ISession @this, string key, out T value) + { + if (@this.TryGetValue(key, out var foundValue) && foundValue is T typedValue) + { + value = typedValue; + return true; + } + +#pragma warning disable CS8653 // "default" can be null - We are returning false, so value is undefined + value = default; +#pragma warning restore CS8653 + return false; + } + + /// Gets the value associated with the specified key. + /// The desired type of the value. + /// The on which this method is called. + /// The key whose value to get from the session. + /// The value associated with the specified key, + /// if the key is found and the associated value is of type ; + /// otherwise, the default value for . + public static T GetValue(this ISession @this, string key) + => @this.TryGetValue(key, out var value) && value is T typedValue ? typedValue : default; + + /// Gets the value associated with the specified key. + /// The desired type of the value. + /// The on which this method is called. + /// The key whose value to get from the session. + /// The default value to return if the key is not found + /// or its associated value is not of type . + /// The value associated with the specified key, + /// if the key is found and the associated value is of type ; + /// otherwise, . + public static T GetOrDefault(this ISession @this, string key, T defaultValue) + => @this.TryGetValue(key, out var value) && value is T typedValue ? typedValue : defaultValue; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Sessions/SessionProxy.cs b/Vendor/EmbedIO-3.5.2/Sessions/SessionProxy.cs new file mode 100644 index 0000000..8637f39 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Sessions/SessionProxy.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using EmbedIO.Sessions.Internal; + +namespace EmbedIO.Sessions +{ + /// + /// Provides the same interface as a session object, + /// plus a basic interface to a session manager. + /// + /// + /// A session proxy can be used just as if it were a session object. + /// A session is automatically created wherever its data are accessed. + /// + /// + public sealed class SessionProxy : ISessionProxy + { + private readonly IHttpContext _context; + private readonly ISessionManager? _sessionManager; + + private ISession? _session; + private bool _onCloseRegistered; + + internal SessionProxy(IHttpContext context, ISessionManager? sessionManager) + { + _context = context; + _sessionManager = sessionManager; + } + + /// + /// Returns a "dummy" interface that will always behave as if no session manager has been defined. + /// + /// + /// The returned interface is only useful + /// to initialize a non-nullable property of type . + /// + public static ISessionProxy None => DummySessionProxy.Instance; + + /// + public bool Exists => _session != null; + + /// + public string Id + { + get + { + EnsureSessionExists(); + return _session!.Id; + } + } + + /// + public TimeSpan Duration + { + get + { + EnsureSessionExists(); + return _session!.Duration; + } + } + + /// + public DateTime LastActivity + { + get + { + EnsureSessionExists(); + return _session!.LastActivity; + } + } + + /// + public int Count => _session?.Count ?? 0; + + /// + public bool IsEmpty => _session?.IsEmpty ?? true; + + /// + public object this[string key] + { + get + { + EnsureSessionExists(); + return _session![key]; + } + set + { + EnsureSessionExists(); + _session![key] = value; + } + } + + /// + public void Delete() + { + EnsureSessionExists(); + + if (_session == null) + return; + + _sessionManager!.Delete(_context, _session.Id); + _session = null; + } + + /// + public void Regenerate() + { + if (_session != null) + _sessionManager!.Delete(_context, _session.Id); + + EnsureSessionManagerExists(); + _session = _sessionManager!.Create(_context); + } + + /// + public void Clear() => _session?.Clear(); + + /// + public bool ContainsKey(string key) + { + EnsureSessionExists(); + return _session!.ContainsKey(key); + } + + /// + public bool TryGetValue(string key, out object value) + { + EnsureSessionExists(); + return _session!.TryGetValue(key, out value); + } + + /// + public bool TryRemove(string key, out object value) + { + EnsureSessionExists(); + return _session!.TryRemove(key, out value); + } + + /// + public IReadOnlyList> TakeSnapshot() + { + EnsureSessionExists(); + return _session!.TakeSnapshot(); + } + + private void EnsureSessionManagerExists() + { + if (_sessionManager == null) + throw new InvalidOperationException("No session manager registered in the web server."); + } + + private void EnsureSessionExists() + { + if (_session != null) + return; + + EnsureSessionManagerExists(); + _session = _sessionManager!.Create(_context); + + if (_onCloseRegistered) + return; + + _context.OnClose(_sessionManager.OnContextClose); + _onCloseRegistered = true; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/ComponentCollectionExtensions.cs b/Vendor/EmbedIO-3.5.2/Utilities/ComponentCollectionExtensions.cs new file mode 100644 index 0000000..a814cba --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/ComponentCollectionExtensions.cs @@ -0,0 +1,20 @@ +using System; + +namespace EmbedIO.Utilities +{ + /// + /// Provides extension methods for types implementing . + /// + public static class ComponentCollectionExtensions + { + /// + /// Adds the specified component to a collection, without giving it a name. + /// + /// The type of components in the collection. + /// The on which this method is called. + /// The component to add. + /// is . + /// + public static void Add(this IComponentCollection @this, T component) => @this.Add(null, component); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/ComponentCollection`1.cs b/Vendor/EmbedIO-3.5.2/Utilities/ComponentCollection`1.cs new file mode 100644 index 0000000..4a5dd98 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/ComponentCollection`1.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Swan.Configuration; + +namespace EmbedIO.Utilities +{ + /// + /// Implements a collection of components. + /// Each component in the collection may be given a unique name for later retrieval. + /// + /// The type of components in the collection. + /// + public class ComponentCollection : ConfiguredObject, IComponentCollection + { + private readonly List _components = new List(); + + private readonly List<(string, T)> _componentsWithSafeNames = new List<(string, T)>(); + + private readonly Dictionary _namedComponents = new Dictionary(); + + /// + public int Count => _components.Count; + + /// + public IReadOnlyDictionary Named => _namedComponents; + + /// + public IReadOnlyList<(string SafeName, T Component)> WithSafeNames => _componentsWithSafeNames; + + /// + public T this[int index] => _components[index]; + + /// + public T this[string key] => _namedComponents[key]; + + /// + public IEnumerator GetEnumerator() => _components.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_components).GetEnumerator(); + + /// + /// The collection is locked. + public void Add(string? name, T component) + { + EnsureConfigurationNotLocked(); + + if (name != null) + { + if (name.Length == 0) + throw new ArgumentException("Component name is empty.", nameof(name)); + + if (_namedComponents.ContainsKey(name)) + throw new ArgumentException("Duplicate component name.", nameof(name)); + } + + if (component == null) + throw new ArgumentNullException(nameof(component)); + + if (_components.Contains(component)) + throw new ArgumentException("Component has already been added.", nameof(component)); + + _components.Add(component); + _componentsWithSafeNames.Add((name ?? $"<{component.GetType().Name}>", component)); + + if (name != null) + _namedComponents.Add(name, component); + } + + /// + /// Locks the collection, preventing further additions. + /// + public void Lock() => LockConfiguration(); + } +} diff --git a/Vendor/EmbedIO-3.5.2/Utilities/DisposableComponentCollection`1.cs b/Vendor/EmbedIO-3.5.2/Utilities/DisposableComponentCollection`1.cs new file mode 100644 index 0000000..99e990b --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/DisposableComponentCollection`1.cs @@ -0,0 +1,49 @@ +using System; + +namespace EmbedIO.Utilities +{ + /// + /// Implements a collection of components that automatically disposes each component + /// implementing . + /// Each component in the collection may be given a unique name for later retrieval. + /// + /// The type of components in the collection. + /// + /// + public class DisposableComponentCollection : ComponentCollection, IDisposable + { + /// + /// Finalizes an instance of the class. + /// + ~DisposableComponentCollection() + { + Dispose(false); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// + /// to release both managed and unmanaged resources; to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (!disposing) return; + + foreach (var component in this) + { + if (component is IDisposable disposable) + disposable.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/HttpDate.cs b/Vendor/EmbedIO-3.5.2/Utilities/HttpDate.cs new file mode 100644 index 0000000..529e4f0 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/HttpDate.cs @@ -0,0 +1,78 @@ +using System; +using System.Globalization; + +namespace EmbedIO.Utilities +{ + /// + /// Provides standard methods to parse and format s according to various RFCs. + /// + public static class HttpDate + { + // https://github.com/dotnet/corefx/blob/master/src/Common/src/System/Net/HttpDateParser.cs + private static readonly string[] DateFormats = { + // "r", // RFC 1123, required output format but too strict for input + "ddd, d MMM yyyy H:m:s 'GMT'", // RFC 1123 (r, except it allows both 1 and 01 for date and time) + "ddd, d MMM yyyy H:m:s 'UTC'", // RFC 1123, UTC + "ddd, d MMM yyyy H:m:s", // RFC 1123, no zone - assume GMT + "d MMM yyyy H:m:s 'GMT'", // RFC 1123, no day-of-week + "d MMM yyyy H:m:s 'UTC'", // RFC 1123, UTC, no day-of-week + "d MMM yyyy H:m:s", // RFC 1123, no day-of-week, no zone + "ddd, d MMM yy H:m:s 'GMT'", // RFC 1123, short year + "ddd, d MMM yy H:m:s 'UTC'", // RFC 1123, UTC, short year + "ddd, d MMM yy H:m:s", // RFC 1123, short year, no zone + "d MMM yy H:m:s 'GMT'", // RFC 1123, no day-of-week, short year + "d MMM yy H:m:s 'UTC'", // RFC 1123, UTC, no day-of-week, short year + "d MMM yy H:m:s", // RFC 1123, no day-of-week, short year, no zone + + "dddd, d'-'MMM'-'yy H:m:s 'GMT'", // RFC 850 + "dddd, d'-'MMM'-'yy H:m:s 'UTC'", // RFC 850, UTC + "dddd, d'-'MMM'-'yy H:m:s zzz", // RFC 850, offset + "dddd, d'-'MMM'-'yy H:m:s", // RFC 850 no zone + "ddd MMM d H:m:s yyyy", // ANSI C's asctime() format + + "ddd, d MMM yyyy H:m:s zzz", // RFC 5322 + "ddd, d MMM yyyy H:m:s", // RFC 5322 no zone + "d MMM yyyy H:m:s zzz", // RFC 5322 no day-of-week + "d MMM yyyy H:m:s", // RFC 5322 no day-of-week, no zone + }; + + /// + /// Attempts to parse a string containing a date and time, and possibly a time zone offset, + /// in one of the formats specified in RFC850, + /// RFC1123, + /// and RFC5322, + /// or ANSI C's asctime() format. + /// + /// The string to parse. + /// When this method returns , + /// a representing the parsed date, time, and time zone offset. + /// This parameter is passed uninitialized. + /// if was successfully parsed; + /// otherwise, . + public static bool TryParse(string str, out DateTimeOffset result) => + DateTimeOffset.TryParseExact( + str, + DateFormats, + DateTimeFormatInfo.InvariantInfo, + DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, + out result); + + /// + /// Formats the specified + /// according to RFC1123. + /// + /// The to format. + /// A string containing the formatted . + public static string Format(DateTimeOffset dateTimeOffset) + => dateTimeOffset.ToUniversalTime().ToString("r", DateTimeFormatInfo.InvariantInfo); + + /// + /// Formats the specified + /// according to RFC1123. + /// + /// The to format. + /// A string containing the formatted . + public static string Format(DateTime dateTime) + => dateTime.ToUniversalTime().ToString("r", DateTimeFormatInfo.InvariantInfo); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/IComponentCollection`1.cs b/Vendor/EmbedIO-3.5.2/Utilities/IComponentCollection`1.cs new file mode 100644 index 0000000..8d60b7b --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/IComponentCollection`1.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; + +namespace EmbedIO.Utilities +{ + /// + /// Represents a collection of components. + /// Each component in the collection may be given a unique name for later retrieval. + /// + /// The type of components in the collection. + public interface IComponentCollection : IReadOnlyList + { + /// + /// Gets an interface representing the named components. + /// + /// + /// The named components. + /// + IReadOnlyDictionary Named { get; } + + /// + /// Gets an interface representing all components + /// associated with safe names. + /// The safe name of a component is never . + /// If a component's unique name if , its safe name + /// will be some non- string somehow identifying it. + /// Note that safe names are not necessarily unique. + /// + /// + /// A list of s, each containing a safe name and a component. + /// + IReadOnlyList<(string SafeName, T Component)> WithSafeNames { get; } + + /// + /// Gets the component with the specified name. + /// + /// + /// The component. + /// + /// The name. + /// The component with the specified . + /// is null. + /// The property is retrieved and is not found. + T this[string name] { get; } + + /// + /// Adds a component to the collection, + /// giving it the specified if it is not . + /// + /// The name given to the module, or . + /// The component. + void Add(string? name, T component); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/IPParser.cs b/Vendor/EmbedIO-3.5.2/Utilities/IPParser.cs new file mode 100644 index 0000000..f0d9bf8 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/IPParser.cs @@ -0,0 +1,209 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Swan.Logging; + +namespace EmbedIO.Utilities +{ + /// + /// Provides standard methods to parse IP address strings. + /// + public static class IPParser + { + /// + /// Parses the specified IP address. + /// + /// The IP address. + /// A collection of parsed correctly from . + public static async Task> ParseAsync(string address) + { + if (address == null) + return Enumerable.Empty(); + + if (IPAddress.TryParse(address, out var ip)) + return new List { ip }; + + try + { + return await Dns.GetHostAddressesAsync(address).ConfigureAwait(false); + } + catch (SocketException socketEx) + { + socketEx.Log(nameof(IPParser)); + } + catch + { + // Ignore + } + + if (IsCidrNotation(address)) + return ParseCidrNotation(address); + + return IsSimpleIPRange(address) ? TryParseSimpleIPRange(address) : Enumerable.Empty(); + } + + /// + /// Determines whether the IP-range string is in CIDR notation. + /// + /// The IP-range string. + /// + /// true if the IP-range string is CIDR notation; otherwise, false. + /// + public static bool IsCidrNotation(string range) + { + if (string.IsNullOrWhiteSpace(range)) + return false; + + var parts = range.Split('/'); + if (parts.Length != 2) + return false; + + var prefix = parts[0]; + var prefixLen = parts[1]; + + var prefixParts = prefix.Split('.'); + if (prefixParts.Length != 4) + return false; + + return byte.TryParse(prefixLen, out var len) && len <= 32; + } + + /// + /// Parse IP-range string in CIDR notation. For example "12.15.0.0/16". + /// + /// The IP-range string. + /// A collection of parsed correctly from . + public static IEnumerable ParseCidrNotation(string range) + { + if (!IsCidrNotation(range)) + return Enumerable.Empty(); + + var parts = range.Split('/'); + var prefix = parts[0]; + + if (!byte.TryParse(parts[1], out var prefixLen)) + return Enumerable.Empty(); + + var prefixParts = prefix.Split('.'); + if (prefixParts.Select(x => byte.TryParse(x, out _)).Any(x => !x)) + return Enumerable.Empty(); + + uint ip = 0; + for (var i = 0; i < 4; i++) + { + ip <<= 8; + ip += uint.Parse(prefixParts[i], NumberFormatInfo.InvariantInfo); + } + + var shiftBits = (byte)(32 - prefixLen); + var ip1 = (ip >> shiftBits) << shiftBits; + + if ((ip1 & ip) != ip1) // Check correct subnet address + return Enumerable.Empty(); + + var ip2 = ip1 >> shiftBits; + for (var k = 0; k < shiftBits; k++) + { + ip2 = (ip2 << 1) + 1; + } + + var beginIP = new byte[4]; + var endIP = new byte[4]; + + for (var i = 0; i < 4; i++) + { + beginIP[i] = (byte)((ip1 >> ((3 - i) * 8)) & 255); + endIP[i] = (byte)((ip2 >> ((3 - i) * 8)) & 255); + } + + return GetAllIPAddresses(beginIP, endIP); + } + + /// + /// Determines whether the IP-range string is in simple IP range notation. + /// + /// The IP-range string. + /// + /// true if the IP-range string is in simple IP range notation; otherwise, false. + /// + public static bool IsSimpleIPRange(string range) + { + if (string.IsNullOrWhiteSpace(range)) + return false; + + var parts = range.Split('.'); + if (parts.Length != 4) + return false; + + foreach (var part in parts) + { + var rangeParts = part.Split('-'); + if (rangeParts.Length < 1 || rangeParts.Length > 2) + return false; + + if (!byte.TryParse(rangeParts[0], out _) || + (rangeParts.Length > 1 && !byte.TryParse(rangeParts[1], out _))) + return false; + } + + return true; + } + + /// + /// Tries to parse IP-range string "12.15-16.1-30.10-255" + /// + /// The IP-range string. + /// A collection of parsed correctly from . + public static IEnumerable TryParseSimpleIPRange(string range) + { + if (!IsSimpleIPRange(range)) + return Enumerable.Empty(); + + var beginIP = new byte[4]; + var endIP = new byte[4]; + + var parts = range.Split('.'); + for (var i = 0; i < 4; i++) + { + var rangeParts = parts[i].Split('-'); + beginIP[i] = byte.Parse(rangeParts[0], NumberFormatInfo.InvariantInfo); + endIP[i] = (rangeParts.Length == 1) ? beginIP[i] : byte.Parse(rangeParts[1], NumberFormatInfo.InvariantInfo); + } + + return GetAllIPAddresses(beginIP, endIP); + } + + private static IEnumerable GetAllIPAddresses(byte[] beginIP, byte[] endIP) + { + for (var i = 0; i < 4; i++) + { + if (endIP[i] < beginIP[i]) + return Enumerable.Empty(); + } + + var capacity = 1; + for (var i = 0; i < 4; i++) + capacity *= endIP[i] - beginIP[i] + 1; + + var ips = new List(capacity); + for (int i0 = beginIP[0]; i0 <= endIP[0]; i0++) + { + for (int i1 = beginIP[1]; i1 <= endIP[1]; i1++) + { + for (int i2 = beginIP[2]; i2 <= endIP[2]; i2++) + { + for (int i3 = beginIP[3]; i3 <= endIP[3]; i3++) + { + ips.Add(new IPAddress(new[] { (byte)i0, (byte)i1, (byte)i2, (byte)i3 })); + } + } + } + } + + return ips; + } + } +} diff --git a/Vendor/EmbedIO-3.5.2/Utilities/MimeTypeProviderStack.cs b/Vendor/EmbedIO-3.5.2/Utilities/MimeTypeProviderStack.cs new file mode 100644 index 0000000..d0cc444 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/MimeTypeProviderStack.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace EmbedIO.Utilities +{ + /// + /// Manages a stack of MIME type providers. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + /// + public sealed class MimeTypeProviderStack : IMimeTypeProvider + { + private readonly Stack _providers = new Stack(); + + /// + /// Pushes the specified MIME type provider on the stack. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + /// The interface to push on the stack. + /// is . + public void Push(IMimeTypeProvider provider) + => _providers.Push(Validate.NotNull(nameof(provider), provider)); + + /// + /// Removes the most recently added MIME type provider from the stack. + /// This API supports the EmbedIO infrastructure and is not intended to be used directly from your code. + /// + public void Pop() => _providers.Pop(); + + /// + public string GetMimeType(string extension) + { + var result = _providers.Select(p => p.GetMimeType(extension)) + .FirstOrDefault(m => m != null); + + if (result == null) + _ = MimeType.Associations.TryGetValue(extension, out result); + + return result; + } + + /// + public bool TryDetermineCompression(string mimeType, out bool preferCompression) + { + foreach (var provider in _providers) + { + if (provider.TryDetermineCompression(mimeType, out preferCompression)) + return true; + } + + preferCompression = default; + return false; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/NameValueCollectionExtensions.cs b/Vendor/EmbedIO-3.5.2/Utilities/NameValueCollectionExtensions.cs new file mode 100644 index 0000000..7596b88 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/NameValueCollectionExtensions.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; + +namespace EmbedIO.Utilities +{ + /// + /// Provides extension methods for . + /// + public static class NameValueCollectionExtensions + { + /// + /// Converts a to a dictionary of objects. + /// Values in the returned dictionary will wither be strings, or arrays of strings, + /// depending on the presence of multiple values for the same key in the collection. + /// + /// The on which this method is called. + /// A associating the collection's keys + /// with their values. + /// is . + public static Dictionary ToDictionary(this NameValueCollection @this) + => @this.Keys.Cast().ToDictionary(key => key, key => { + var values = @this.GetValues(key); + if (values == null) + return null; + + return values.Length switch { + 0 => null, + 1 => (object) values[0], + _ => (object) values + }; + }); + + /// + /// Converts a to a dictionary of strings. + /// + /// The on which this method is called. + /// A associating the collection's keys + /// with their values (or comma-separated lists in case of multiple values). + /// is . + public static Dictionary ToStringDictionary(this NameValueCollection @this) + => @this.Keys.Cast().ToDictionary(key => key, @this.Get); + + /// + /// Converts a to a dictionary of arrays of strings. + /// + /// The on which this method is called. + /// A associating the collection's keys + /// with arrays of their values. + /// is . + public static Dictionary ToArrayDictionary(this NameValueCollection @this) + => @this.Keys.Cast().ToDictionary(key => key, @this.GetValues); + + /// + /// Determines whether a contains one or more values + /// for the specified . + /// + /// The on which this method is called. + /// The key to look for. + /// if at least one value for + /// is present in the collection; otherwise, . + /// + /// is . + public static bool ContainsKey(this NameValueCollection @this, string key) + => @this.Keys.Cast().Contains(key); + + /// + /// Determines whether a contains one or more values + /// for the specified , at least one of which is equal to the specified + /// . Value comparisons are carried out using the + /// comparison type. + /// + /// The on which this method is called. + /// The name to look for. + /// The value to look for. + /// if at least one of the values for + /// in the collection is equal to ; otherwise, . + /// + /// is . + /// White space is trimmed from the start and end of each value before comparison. + /// + public static bool Contains(this NameValueCollection @this, string name, string value) + => Contains(@this, name, value, StringComparison.OrdinalIgnoreCase); + + /// + /// Determines whether a contains one or more values + /// for the specified , at least one of which is equal to the specified + /// . Value comparisons are carried out using the specified + /// . + /// + /// The on which this method is called. + /// The name to look for. + /// The value to look for. + /// One of the enumeration values + /// that specifies how the strings will be compared. + /// if at least one of the values for + /// in the collection is equal to ; otherwise, . + /// + /// is . + /// White space is trimmed from the start and end of each value before comparison. + /// + public static bool Contains(this NameValueCollection @this, string name, string? value, StringComparison comparisonType) + { + value = value?.Trim(); + return @this[name]?.SplitByComma() + .Any(val => string.Equals(val?.Trim(), value, comparisonType)) ?? false; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/QValueList.cs b/Vendor/EmbedIO-3.5.2/Utilities/QValueList.cs new file mode 100644 index 0000000..5d43299 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/QValueList.cs @@ -0,0 +1,279 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace EmbedIO.Utilities +{ + /// + /// Represents a list of names with associated quality values extracted from an HTTP header, + /// e.g. gzip; q=0.9, deflate. + /// See RFC7231, section 5.3. + /// This class ignores and discards extensions (accept-ext in RFC7231 terminology). + /// If a name has one or more parameters (e.g. text/html;level=1) it is not + /// further parsed: parameters will appear as part of the name. + /// + public sealed class QValueList + { + /// + /// A value signifying "anything will do" in request headers. + /// For example, a request header of + /// Accept-Encoding: *;q=0.8, gzip means "I prefer GZip compression; + /// if it is not available, any other compression (including no compression at all) + /// is OK for me". + /// + public const string Wildcard = "*"; + + // This will match a quality value between two semicolons + // or between a semicolon and the end of a string. + // Match groups will be: + // Groups[0] = The matching string + // Groups[1] = If group is successful, "0"; otherwise, the weight is 1.000 + // Groups[2] = If group is successful, the decimal digits after 0 + // The part of string before the match contains the value and parameters (if any). + // The part of string after the match contains the extensions (if any). + // If there is no match, the whole string is just value and parameters (if any). + private static readonly Regex QualityValueRegex = new Regex( + @";[ \t]*q=(?:(?:1(?:\.(?:0{1,3}))?)|(?:(0)(?:\.(\d{1,3}))?))[ \t]*(?:;|,|$)", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Singleline); + + /// + /// Initializes a new instance of the class + /// by parsing comma-separated request header values. + /// + /// If set to , a value of * + /// will be treated as signifying "anything". + /// A list of comma-separated header values. + /// + public QValueList(bool useWildcard, string headerValues) + { + UseWildcard = useWildcard; + QValues = Parse(headerValues); + } + + /// + /// Initializes a new instance of the class + /// by parsing comma-separated request header values. + /// + /// If set to , a value of * + /// will be treated as signifying "anything". + /// An enumeration of header values. + /// Note that each element of the enumeration may in turn be + /// a comma-separated list. + /// + public QValueList(bool useWildcard, IEnumerable headerValues) + { + UseWildcard = useWildcard; + QValues = Parse(headerValues); + } + + /// + /// Initializes a new instance of the class + /// by parsing comma-separated request header values. + /// + /// If set to , a value of * + /// will be treated as signifying "anything". + /// An array of header values. + /// Note that each element of the array may in turn be + /// a comma-separated list. + /// + public QValueList(bool useWildcard, params string[] headerValues) + : this(useWildcard, headerValues as IEnumerable) + { + } + + /// + /// Gets a dictionary associating values with their relative weight + /// (an integer ranging from 0 to 1000) and their position in the + /// list of header values from which this instance has been constructed. + /// + /// + /// This property does not usually need to be used directly; + /// use the , , + /// , and + /// methods instead. + /// + /// + /// + /// + /// + public IReadOnlyDictionary QValues { get; } + + /// + /// Gets a value indicating whether * is treated as a special value + /// with the meaning of "anything". + /// + public bool UseWildcard { get; } + + /// + /// Determines whether the specified value is a possible candidate. + /// + /// The value. + /// if is a candidate; + /// otherwise, . + public bool IsCandidate(string value) + => TryGetCandidateValue(Validate.NotNull(nameof(value), value), out var candidate) && candidate.Weight > 0; + + /// + /// Attempts to determine whether the weight of a possible candidate. + /// + /// The value whose weight is to be determined. + /// When this method returns , + /// the weight of the candidate. + /// if is a candidate; + /// otherwise, . + public bool TryGetWeight(string value, out int weight) + { + var result = TryGetCandidateValue(Validate.NotNull(nameof(value), value), out var candidate); + weight = candidate.Weight; + return result; + } + + /// + /// Finds the value preferred by the client among an enumeration of values. + /// + /// The values. + /// The value preferred by the client, or + /// if none of the provided is accepted. + public string? FindPreferred(IEnumerable values) + => FindPreferredCore(values, out var result) >= 0 ? result : null; + + /// + /// Finds the index of the value preferred by the client in a list of values. + /// + /// The values. + /// The index of the value preferred by the client, or -1 + /// if none of the values in is accepted. + public int FindPreferredIndex(IEnumerable values) => FindPreferredCore(values, out _); + + /// + /// Finds the index of the value preferred by the client in an array of values. + /// + /// The values. + /// The index of the value preferred by the client, or -1 + /// if none of the values in is accepted. + public int FindPreferredIndex(params string[] values) => FindPreferredIndex(values as IReadOnlyList); + + private static IReadOnlyDictionary Parse(string headerValues) + { + var result = new Dictionary(); + ParseCore(headerValues, result); + return result; + } + + private static IReadOnlyDictionary Parse(IEnumerable headerValues) + { + var result = new Dictionary(); + + if (headerValues == null) return result; + + foreach (var headerValue in headerValues) + ParseCore(headerValue, result); + + return result; + } + + private static void ParseCore(string text, IDictionary dictionary) + { + if (string.IsNullOrEmpty(text)) + return; + + var length = text.Length; + var position = 0; + var ordinal = 0; + while (position < length) + { + var stop = text.IndexOf(',', position); + if (stop < 0) + stop = length; + + string name; + var weight = 1000; + var match = QualityValueRegex.Match(text, position, stop - position); + if (match.Success) + { + var groups = match.Groups; + var wholeMatch = groups[0]; + name = text.Substring(position, wholeMatch.Index - position).Trim(); + if (groups[1].Success) + { + weight = 0; + if (groups[2].Success) + { + var digits = groups[2].Value; + var n = 0; + while (n < digits.Length) + { + weight = (10 * weight) + (digits[n] - '0'); + n++; + } + + while (n < 3) + { + weight = 10 * weight; + n++; + } + } + } + } + else + { + name = text.Substring(position, stop - position).Trim(); + } + + if (!string.IsNullOrEmpty(name)) + dictionary[name] = (weight, ordinal); + + position = stop + 1; + ordinal++; + } + } + + private static int CompareQualities((int Weight, int Ordinal) a, (int Weight, int Ordinal) b) + { + if (a.Weight > b.Weight) + return 1; + + if (a.Weight < b.Weight) + return -1; + + if (a.Ordinal < b.Ordinal) + return 1; + + if (a.Ordinal > b.Ordinal) + return -1; + + return 0; + } + + private int FindPreferredCore(IEnumerable values, out string? result) + { + values = Validate.NotNull(nameof(values), values); + + result = null; + var best = -1; + + // Set initial values such as a weight of 0 can never win over them + (int Weight, int Ordinal) bestValue = (0, int.MinValue); + var i = 0; + foreach (var value in values) + { + if (value == null) + continue; + + if (TryGetCandidateValue(value, out var candidateValue) && CompareQualities(candidateValue, bestValue) > 0) + { + result = value; + best = i; + bestValue = candidateValue; + } + + i++; + } + + return best; + } + + private bool TryGetCandidateValue(string value, out (int Weight, int Ordinal) candidate) + => QValues.TryGetValue(value, out candidate) + || (UseWildcard && QValues.TryGetValue(Wildcard, out candidate)); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/QValueListExtensions.cs b/Vendor/EmbedIO-3.5.2/Utilities/QValueListExtensions.cs new file mode 100644 index 0000000..fb19cbe --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/QValueListExtensions.cs @@ -0,0 +1,72 @@ +namespace EmbedIO.Utilities +{ + /// + /// Provides extension methods for . + /// + public static class QValueListExtensions + { + /// + /// Attempts to proactively negotiate a compression method for a response, + /// based on the contents of a . + /// + /// The on which this method is called. + /// if sending compressed data is preferred over + /// sending non-compressed data; otherwise, . + /// When this method returns, the compression method to use for the response, + /// if content negotiation is successful. This parameter is passed uninitialized. + /// When this method returns, the name of the compression method, + /// if content negotiation is successful. This parameter is passed uninitialized. + /// if content negotiation is successful; + /// otherwise, . + /// + /// If is empty, this method always returns , + /// setting to + /// and to . + /// + public static bool TryNegotiateContentEncoding( + this QValueList @this, + bool preferCompression, + out CompressionMethod compressionMethod, + out string? compressionMethodName) + { + if (@this.QValues.Count < 1) + { + compressionMethod = CompressionMethod.None; + compressionMethodName = CompressionMethodNames.None; + return true; + } + + // https://tools.ietf.org/html/rfc7231#section-5.3.4 + // RFC7231, Section 5.3.4, rule #2: + // If the representation has no content-coding, then it is + // acceptable by default unless specifically excluded by the + // Accept - Encoding field stating either "identity;q=0" or "*;q=0" + // without a more specific entry for "identity". + if (!preferCompression && (!@this.TryGetWeight(CompressionMethodNames.None, out var weight) || weight > 0)) + { + compressionMethod = CompressionMethod.None; + compressionMethodName = CompressionMethodNames.None; + return true; + } + + var acceptableMethods = preferCompression + ? new[] { CompressionMethod.Gzip, CompressionMethod.Deflate, CompressionMethod.None } + : new[] { CompressionMethod.None, CompressionMethod.Gzip, CompressionMethod.Deflate }; + var acceptableMethodNames = preferCompression + ? new[] { CompressionMethodNames.Gzip, CompressionMethodNames.Deflate, CompressionMethodNames.None } + : new[] { CompressionMethodNames.None, CompressionMethodNames.Gzip, CompressionMethodNames.Deflate }; + + var acceptableMethodIndex = @this.FindPreferredIndex(acceptableMethodNames); + if (acceptableMethodIndex < 0) + { + compressionMethod = default; + compressionMethodName = default; + return false; + } + + compressionMethod = acceptableMethods[acceptableMethodIndex]; + compressionMethodName = acceptableMethodNames[acceptableMethodIndex]; + return true; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/StringExtensions.cs b/Vendor/EmbedIO-3.5.2/Utilities/StringExtensions.cs new file mode 100644 index 0000000..937430b --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/StringExtensions.cs @@ -0,0 +1,55 @@ +using System; + +namespace EmbedIO.Utilities +{ + /// + /// Provides extension methods for . + /// + public static class StringExtensions + { + private static readonly char[] CommaSplitChars = {','}; + + /// Splits a string into substrings based on the specified . + /// The returned array includes empty array elements if two or more consecutive delimiters are found + /// in . + /// The on which this method is called. + /// An array of s to use as delimiters. + /// An array whose elements contain the substrings in that are delimited + /// by one or more characters in . + /// is . + public static string[] SplitByAny(this string @this, params char[] delimiters) => @this.Split(delimiters); + + /// Splits a string into substrings, using the comma (,) character as a delimiter. + /// The returned array includes empty array elements if two or more commas are found in . + /// The on which this method is called. + /// An array whose elements contain the substrings in that are delimited by commas. + /// is . + /// + public static string[] SplitByComma(this string @this) => @this.Split(CommaSplitChars); + + /// Splits a string into substrings, using the comma (,) character as a delimiter. + /// You can specify whether the substrings include empty array elements. + /// The on which this method is called. + /// to omit empty array elements from the array returned; + /// or to include empty array elements in the array returned. + /// + /// An array whose elements contain the substrings in that are delimited by commas. + /// For more information, see the Remarks section of the method. + /// + /// is . + /// options is not one of the values. + /// + public static string[] SplitByComma(this string @this, StringSplitOptions options) => + @this.Split(CommaSplitChars, options); + + /// + /// Ensures that a is never empty, + /// by transforming empty strings into . + /// + /// The on which this method is called. + /// If is the empty string, ; + /// otherwise, + public static string? NullIfEmpty(this string @this) + => string.IsNullOrEmpty(@this) ? null : @this; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/UniqueIdGenerator.cs b/Vendor/EmbedIO-3.5.2/Utilities/UniqueIdGenerator.cs new file mode 100644 index 0000000..66b5ba9 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/UniqueIdGenerator.cs @@ -0,0 +1,16 @@ +using System; + +namespace EmbedIO.Utilities +{ + /// + /// Generates locally unique string IDs, mainly for logging purposes. + /// + public static class UniqueIdGenerator + { + /// + /// Generates and returns a unique ID. + /// + /// The generated ID. + public static string GetNext() => Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Substring(0, 22); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/UrlEncodedDataParser.cs b/Vendor/EmbedIO-3.5.2/Utilities/UrlEncodedDataParser.cs new file mode 100644 index 0000000..1963cca --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/UrlEncodedDataParser.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Specialized; +using System.Net; +using EmbedIO.Internal; + +namespace EmbedIO.Utilities +{ + /// + /// Parses URL queries or URL-encoded HTML forms. + /// + public static class UrlEncodedDataParser + { + /// + /// Parses a URL query or URL-encoded HTML form. + /// Unlike , the returned + /// will have bracketed indexes stripped away; + /// for example, a[0]=1&a[1]=2 will yield the same result as a=1&a=2, + /// i.e. a with one key (a) associated with + /// two values (1 and 2). + /// + /// The string to parse. + /// If this parameter is , + /// tokens not followed by an equal sign (e.g. this in a=1&this&b=2) + /// will be grouped as values of a null key. + /// This is the same behavior as the and + /// properties. + /// If this parameter is , tokens not followed by an equal sign + /// (e.g. this in a=1&this&b=2) will be considered keys with an empty + /// value. This is the same behavior as the + /// extension method. + /// (the default) to return + /// a mutable (non-read-only) collection; to return a read-only collection. + /// A containing the parsed data. + public static NameValueCollection Parse(string source, bool groupFlags, bool mutableResult = true) + { + var result = new LockableNameValueCollection(); + + // Verify there is data to parse; otherwise, return an empty collection. + if (string.IsNullOrEmpty(source)) + { + if (!mutableResult) + result.MakeReadOnly(); + + return result; + } + + void AddKeyValuePair(string? key, string value) + { + if (key != null) + { + // Decode the key. + key = WebUtility.UrlDecode(key); + + // Discard bracketed index (used e.g. by PHP) + var bracketPos = key.IndexOf("[", StringComparison.Ordinal); + if (bracketPos > 0) + key = key.Substring(0, bracketPos); + } + + // Decode the value. + value = WebUtility.UrlDecode(value); + + // Add the KVP to the collection. + result.Add(key, value); + } + + // Skip the initial question mark, + // in case source is the Query property of a Uri. + var kvpPos = source[0] == '?' ? 1 : 0; + var length = source.Length; + while (kvpPos < length) + { + var separatorPos = kvpPos; + var equalPos = -1; + + while (separatorPos < length) + { + var c = source[separatorPos]; + if (c == '&') + break; + + if (c == '=' && equalPos < 0) + equalPos = separatorPos; + + separatorPos++; + } + + // Split by the equals char into key and value. + // Some KVPS will have only their key, some will have both key and value + // Some other might be repeated which really means an array + if (equalPos < 0) + { + if (groupFlags) + { + AddKeyValuePair(null, source.Substring(kvpPos, separatorPos - kvpPos)); + } + else + { + AddKeyValuePair(source.Substring(kvpPos, separatorPos - kvpPos), string.Empty); + } + } + else + { + AddKeyValuePair( + source.Substring(kvpPos, equalPos - kvpPos), + source.Substring(equalPos + 1, separatorPos - equalPos - 1)); + } + + // Edge case: if the last character in source is '&', + // there's an empty KVP that we would otherwise skip. + if (separatorPos == length - 1) + { + AddKeyValuePair(groupFlags ? null : string.Empty, string.Empty); + break; + } + + // On to next KVP + kvpPos = separatorPos + 1; + } + + if (!mutableResult) + result.MakeReadOnly(); + + return result; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/UrlPath.cs b/Vendor/EmbedIO-3.5.2/Utilities/UrlPath.cs new file mode 100644 index 0000000..82d6905 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/UrlPath.cs @@ -0,0 +1,315 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace EmbedIO.Utilities +{ + /// + /// Provides utility methods to work with URL paths. + /// + public static class UrlPath + { + /// + /// The root URL path value, i.e. "/". + /// + public const string Root = "/"; + + private static readonly Regex MultipleSlashRegex = new Regex("//+", RegexOptions.Compiled | RegexOptions.CultureInvariant); + + /// + /// Determines whether a string is a valid URL path. + /// + /// The URL path. + /// + /// if the specified URL path is valid; otherwise, . + /// + /// + /// For a string to be a valid URL path, it must not be , + /// must not be empty, and must start with a slash (/) character. + /// To ensure that a method parameter is a valid URL path, use . + /// + /// + /// + /// + public static bool IsValid(string urlPath) => ValidateInternal(nameof(urlPath), urlPath) == null; + + /// + /// Normalizes the specified URL path. + /// + /// The URL path. + /// if set to , treat the URL path + /// as a base path, i.e. ensure it ends with a slash (/) character; + /// otherwise, ensure that it does NOT end with a slash character. + /// The normalized path. + /// + /// is not a valid URL path. + /// + /// + /// A normalized URL path is one where each run of two or more slash + /// (/) characters has been replaced with a single slash character. + /// This method does NOT try to decode URL-encoded characters. + /// If you are sure that is a valid URL path, + /// for example because you have called and it returned + /// , then you may call + /// instead of this method. is slightly faster because + /// it skips the initial validity check. + /// There is no need to call this method for a method parameter + /// for which you have already called . + /// + /// + /// + /// + public static string Normalize(string urlPath, bool isBasePath) + { + var exception = ValidateInternal(nameof(urlPath), urlPath); + if (exception != null) + throw exception; + + return UnsafeNormalize(urlPath, isBasePath); + } + + /// + /// Normalizes the specified URL path, assuming that it is valid. + /// + /// The URL path. + /// if set to , treat the URL path + /// as a base path, i.e. ensure it ends with a slash (/) character; + /// otherwise, ensure that it does NOT end with a slash character. + /// The normalized path. + /// + /// A normalized URL path is one where each run of two or more slash + /// (/) characters has been replaced with a single slash character. + /// This method does NOT try to decode URL-encoded characters. + /// If is not valid, the behavior of + /// this method is unspecified. You should call this method only after + /// has returned + /// for the same . + /// You should call instead of this method + /// if you are not sure that is valid. + /// There is no need to call this method for a method parameter + /// for which you have already called . + /// + /// + /// + /// + public static string UnsafeNormalize(string urlPath, bool isBasePath) + { + // Replace each run of multiple slashes with a single slash + urlPath = MultipleSlashRegex.Replace(urlPath, "/"); + + // The root path needs no further checking. + var length = urlPath.Length; + if (length == 1) + return urlPath; + + // Base URL paths must end with a slash; + // non-base URL paths must NOT end with a slash. + // The final slash is irrelevant for the URL itself + // (it has to map the same way with or without it) + // but makes comparing and mapping URLs a lot simpler. + var finalPosition = length - 1; + var endsWithSlash = urlPath[finalPosition] == '/'; + return isBasePath + ? (endsWithSlash ? urlPath : urlPath + "/") + : (endsWithSlash ? urlPath.Substring(0, finalPosition) : urlPath); + } + + /// + /// Determines whether the specified URL path is prefixed by the specified base URL path. + /// + /// The URL path. + /// The base URL path. + /// + /// if is prefixed by ; + /// otherwise, . + /// + /// + /// is not a valid URL path. + /// - or - + /// is not a valid base URL path. + /// + /// + /// This method returns even if the two URL paths are equivalent, + /// for example if both are "/", or if is "/download" and + /// is "/download/". + /// If you are sure that both and + /// are valid and normalized, for example because you have called , + /// then you may call instead of this method. + /// is slightly faster because it skips validity checks. + /// + /// + /// + /// + /// + public static bool HasPrefix(string urlPath, string baseUrlPath) + => UnsafeHasPrefix( + Validate.UrlPath(nameof(urlPath), urlPath, false), + Validate.UrlPath(nameof(baseUrlPath), baseUrlPath, true)); + + /// + /// Determines whether the specified URL path is prefixed by the specified base URL path, + /// assuming both paths are valid and normalized. + /// + /// The URL path. + /// The base URL path. + /// + /// if is prefixed by ; + /// otherwise, . + /// + /// + /// Unless both and are valid, + /// normalized URL paths, the behavior of this method is unspecified. You should call this method + /// only after calling either or + /// to check and normalize both parameters. + /// If you are not sure about the validity and/or normalization of parameters, + /// call instead of this method. + /// This method returns even if the two URL paths are equivalent, + /// for example if both are "/", or if is "/download" and + /// is "/download/". + /// + /// + /// + /// + /// + public static bool UnsafeHasPrefix(string urlPath, string baseUrlPath) + => urlPath.StartsWith(baseUrlPath, StringComparison.Ordinal) + || (urlPath.Length == baseUrlPath.Length - 1 && baseUrlPath.StartsWith(urlPath, StringComparison.Ordinal)); + + /// + /// Strips a base URL path fom a URL path, obtaining a relative path. + /// + /// The URL path. + /// The base URL path. + /// The relative path, or if + /// is not prefixed by . + /// + /// is not a valid URL path. + /// - or - + /// is not a valid base URL path. + /// + /// + /// The returned relative path is NOT prefixed by a slash (/) character. + /// If and are equivalent, + /// for example if both are "/", or if is "/download" + /// and is "/download/", this method returns an empty string. + /// If you are sure that both and + /// are valid and normalized, for example because you have called , + /// then you may call instead of this method. + /// is slightly faster because it skips validity checks. + /// + /// + /// + /// + /// + public static string? StripPrefix(string urlPath, string baseUrlPath) + => UnsafeStripPrefix( + Validate.UrlPath(nameof(urlPath), urlPath, false), + Validate.UrlPath(nameof(baseUrlPath), baseUrlPath, true)); + + /// + /// Strips a base URL path fom a URL path, obtaining a relative path, + /// assuming both paths are valid and normalized. + /// + /// The URL path. + /// The base URL path. + /// The relative path, or if + /// is not prefixed by . + /// + /// Unless both and are valid, + /// normalized URL paths, the behavior of this method is unspecified. You should call this method + /// only after calling either or + /// to check and normalize both parameters. + /// If you are not sure about the validity and/or normalization of parameters, + /// call instead of this method. + /// The returned relative path is NOT prefixed by a slash (/) character. + /// If and are equivalent, + /// for example if both are "/", or if is "/download" + /// and is "/download/", this method returns an empty string. + /// + /// + /// + /// + /// + public static string? UnsafeStripPrefix(string urlPath, string baseUrlPath) + { + if (!UnsafeHasPrefix(urlPath, baseUrlPath)) + return null; + + // The only case where UnsafeHasPrefix returns true for a urlPath shorter than baseUrlPath + // is urlPath == (baseUrlPath minus the final slash). + return urlPath.Length < baseUrlPath.Length + ? string.Empty + : urlPath.Substring(baseUrlPath.Length); + } + + /// + /// Splits the specified URL path into segments. + /// + /// The URL path. + /// An enumeration of path segments. + /// is not a valid URL path. + /// + /// A root URL path (/) will result in an empty enumeration. + /// The returned enumeration will be the same whether is a base URL path or not. + /// If you are sure that is valid and normalized, + /// for example because you have called , + /// then you may call instead of this method. + /// is slightly faster because it skips validity checks. + /// + /// + /// + /// + public static IEnumerable Split(string urlPath) + => UnsafeSplit(Validate.UrlPath(nameof(urlPath), urlPath, false)); + + /// + /// Splits the specified URL path into segments, assuming it is valid and normalized. + /// + /// The URL path. + /// An enumeration of path segments. + /// + /// Unless is a valid, normalized URL path, + /// the behavior of this method is unspecified. You should call this method + /// only after calling either or + /// to check and normalize both parameters. + /// If you are not sure about the validity and/or normalization of , + /// call instead of this method. + /// A root URL path (/) will result in an empty enumeration. + /// The returned enumeration will be the same whether is a base URL path or not. + /// + /// + /// + /// + public static IEnumerable UnsafeSplit(string urlPath) + { + var length = urlPath.Length; + var position = 1; // Skip initial slash + while (position < length) + { + var slashPosition = urlPath.IndexOf('/', position); + if (slashPosition < 0) + { + yield return urlPath.Substring(position); + break; + } + + yield return urlPath.Substring(position, slashPosition - position); + position = slashPosition + 1; + } + } + + internal static Exception? ValidateInternal(string argumentName, string value) + { + if (value == null) + return new ArgumentNullException(argumentName); + + if (value.Length == 0) + return new ArgumentException("URL path is empty.", argumentName); + + if (value[0] != '/') + return new ArgumentException("URL path does not start with a slash.", argumentName); + + return null; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/Validate-MimeType.cs b/Vendor/EmbedIO-3.5.2/Utilities/Validate-MimeType.cs new file mode 100644 index 0000000..8f72f90 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/Validate-MimeType.cs @@ -0,0 +1,32 @@ +using System; + +namespace EmbedIO.Utilities +{ + partial class Validate + { + /// + /// Ensures that a argument is valid as MIME type or media range as defined by + /// RFC7231, Section 5,3.2. + /// + /// The name of the argument to validate. + /// The value to validate. + /// If , media ranges (i.e. strings of the form */* + /// and type/*) are considered valid; otherwise, they are rejected as invalid. + /// , if it is a valid MIME type or media range. + /// is . + /// + /// is the empty string. + /// - or - + /// is not a valid MIME type or media range. + /// + public static string MimeType(string argumentName, string value, bool acceptMediaRange) + { + value = NotNullOrEmpty(argumentName, value); + + if (!EmbedIO.MimeType.IsMimeType(value, acceptMediaRange)) + throw new ArgumentException("MIME type is not valid.", argumentName); + + return value; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/Validate-Paths.cs b/Vendor/EmbedIO-3.5.2/Utilities/Validate-Paths.cs new file mode 100644 index 0000000..a875474 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/Validate-Paths.cs @@ -0,0 +1,95 @@ +using System; +using System.IO; +using System.Security; + +namespace EmbedIO.Utilities +{ + partial class Validate + { + private static readonly char[] InvalidLocalPathChars = GetInvalidLocalPathChars(); + + /// + /// Ensures that the value of an argument is a valid URL path + /// and normalizes it. + /// + /// The name of the argument to validate. + /// The value to validate. + /// If set to true, the returned path + /// is ensured to end in a slash (/) character; otherwise, the returned path is + /// ensured to not end in a slash character unless it is "/". + /// The normalized URL path. + /// is . + /// + /// is empty. + /// - or - + /// does not start with a slash (/) character. + /// + /// + public static string UrlPath(string argumentName, string value, bool isBasePath) + { + var exception = Utilities.UrlPath.ValidateInternal(argumentName, value); + if (exception != null) + throw exception; + + return Utilities.UrlPath.Normalize(value, isBasePath); + } + + /// + /// Ensures that the value of an argument is a valid local path + /// and, optionally, gets the corresponding full path. + /// + /// The name of the argument to validate. + /// The value to validate. + /// to get the full path, to leave the path as is.. + /// The local path, or the full path if is . + /// is . + /// + /// is empty. + /// - or - + /// contains only white space. + /// - or - + /// contains one or more invalid characters. + /// - or - + /// is and the full path could not be obtained. + /// + public static string LocalPath(string argumentName, string value, bool getFullPath) + { + if (value == null) + throw new ArgumentNullException(argumentName); + + if (value.Length == 0) + throw new ArgumentException("Local path is empty.", argumentName); + + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("Local path contains only white space.", argumentName); + + if (value.IndexOfAny(InvalidLocalPathChars) >= 0) + throw new ArgumentException("Local path contains one or more invalid characters.", argumentName); + + if (getFullPath) + { + try + { + value = Path.GetFullPath(value); + } + catch (Exception e) when (e is ArgumentException || e is SecurityException || e is NotSupportedException || e is PathTooLongException) + { + throw new ArgumentException("Could not get the full local path.", argumentName, e); + } + } + + return value; + } + + private static char[] GetInvalidLocalPathChars() + { + var systemChars = Path.GetInvalidPathChars(); + var p = systemChars.Length; + var result = new char[p + 2]; + Array.Copy(systemChars, result, p); + result[p++] = '*'; + result[p] = '?'; + return result; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/Validate-Rfc2616.cs b/Vendor/EmbedIO-3.5.2/Utilities/Validate-Rfc2616.cs new file mode 100644 index 0000000..30f97a8 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/Validate-Rfc2616.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; + +namespace EmbedIO.Utilities +{ + partial class Validate + { + private static readonly char[] ValidRfc2616TokenChars = GetValidRfc2616TokenChars(); + + /// + /// Ensures that a argument is valid as a token as defined by + /// RFC2616, Section 2.2. + /// RFC2616 tokens are used, for example, as: + /// + /// cookie names, as stated in RFC6265, Section 4.1.1; + /// WebSocket protocol names, as stated in RFC6455, Section 4.3. + /// + /// Only a restricted set of characters are allowed in tokens, including: + /// + /// upper- and lower-case letters of the English alphabet; + /// decimal digits; + /// the following non-alphanumeric characters: + /// !, #, $, %, &, ', *, +, + /// -, ., ^, _, `, |, ~. + /// + /// + /// The name of the argument to validate. + /// The value to validate. + /// , if it is a valid token. + /// is . + /// + /// is the empty string. + /// - or - + /// contains one or more characters that are not allowed in a token. + /// + public static string Rfc2616Token(string argumentName, string value) + { + value = NotNullOrEmpty(argumentName, value); + + if (!IsRfc2616Token(value)) + throw new ArgumentException("Token contains one or more invalid characters.", argumentName); + + return value; + } + + internal static bool IsRfc2616Token(string value) + => !string.IsNullOrEmpty(value) + && !value.Any(c => c < '\x21' || c > '\x7E' || Array.BinarySearch(ValidRfc2616TokenChars, c) < 0); + + private static char[] GetValidRfc2616TokenChars() + => "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&'*+-.^_`|~" + .OrderBy(c => c) + .ToArray(); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/Validate-Route.cs b/Vendor/EmbedIO-3.5.2/Utilities/Validate-Route.cs new file mode 100644 index 0000000..a007ca1 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/Validate-Route.cs @@ -0,0 +1,33 @@ +using System; + +namespace EmbedIO.Utilities +{ + partial class Validate + { + /// + /// Ensures that the value of an argument is a valid route. + /// + /// The name of the argument to validate. + /// The value to validate. + /// if the argument must be a base route; + /// if the argument must be a non-base route. + /// , if it is a valid route. + /// is . + /// + /// is empty. + /// - or - + /// does not start with a slash (/) character. + /// - or - + /// does not comply with route syntax. + /// + /// + public static string Route(string argumentName, string value, bool isBaseRoute) + { + var exception = Routing.Route.ValidateInternal(argumentName, value, isBaseRoute); + if (exception != null) + throw exception; + + return Utilities.UrlPath.UnsafeNormalize(value, isBaseRoute); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/Utilities/Validate.cs b/Vendor/EmbedIO-3.5.2/Utilities/Validate.cs new file mode 100644 index 0000000..8fb57d7 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/Utilities/Validate.cs @@ -0,0 +1,125 @@ +using System; + +namespace EmbedIO.Utilities +{ + /// + /// Provides validation methods for method arguments. + /// + public static partial class Validate + { + /// + /// Ensures that an argument is not . + /// + /// The type of the argument to validate. + /// The name of the argument to validate. + /// The value to validate. + /// if not . + /// is . + public static T NotNull(string argumentName, T? value) + where T : class + => value ?? throw new ArgumentNullException(argumentName); + + /// + /// Ensures that a argument is neither nor the empty string. + /// + /// The name of the argument to validate. + /// The value to validate. + /// if neither nor the empty string. + /// is . + /// is the empty string. + public static string NotNullOrEmpty(string argumentName, string? value) + { + if (value == null) + throw new ArgumentNullException(argumentName); + + if (value.Length == 0) + throw new ArgumentException("String is empty.", argumentName); + + return value; + } + + /// + /// Ensures that a valid URL can be constructed from a argument. + /// + /// Name of the argument. + /// The value. + /// Specifies whether is a relative URL, absolute URL, or is indeterminate. + /// Ensure that, if is an absolute URL, its scheme is either http or https. + /// The string representation of the constructed URL. + /// is . + /// + /// is not a valid URL. + /// - or - + /// is , is an absolute URL, + /// and 's scheme is neither http nor https. + /// + /// + public static string Url( + string argumentName, + string value, + UriKind uriKind = UriKind.RelativeOrAbsolute, + bool enforceHttp = false) + { + Uri uri; + try + { + uri = new Uri(NotNull(argumentName, value), uriKind); + } + catch (UriFormatException e) + { + throw new ArgumentException("URL is not valid.", argumentName, e); + } + + if (enforceHttp && uri.IsAbsoluteUri && uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) + throw new ArgumentException("URL scheme is neither HTTP nor HTTPS.", argumentName); + + return uri.ToString(); + } + + /// + /// Ensures that a valid URL, either absolute or relative to the given , + /// can be constructed from a argument and returns the absolute URL + /// obtained by combining and . + /// + /// Name of the argument. + /// The value. + /// The base URI for relative URLs. + /// Ensure that the resulting URL's scheme is either http or https. + /// The string representation of the constructed URL. + /// + /// is . + /// - or - + /// is . + /// + /// + /// is not an absolute URI. + /// - or - + /// is not a valid URL. + /// - or - + /// is , + /// and the combination of and has a scheme + /// that is neither http nor https. + /// + /// + public static string Url(string argumentName, string value, Uri baseUri, bool enforceHttp = false) + { + if (!NotNull(nameof(baseUri), baseUri).IsAbsoluteUri) + throw new ArgumentException("Base URI is not an absolute URI.", nameof(baseUri)); + + Uri uri; + try + { + uri = new Uri(baseUri, new Uri(NotNull(argumentName, value), UriKind.RelativeOrAbsolute)); + } + catch (UriFormatException e) + { + throw new ArgumentException("URL is not valid.", argumentName, e); + } + + if (enforceHttp && uri.IsAbsoluteUri && uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) + throw new ArgumentException("URL scheme is neither HTTP nor HTTPS.", argumentName); + + return uri.ToString(); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebApi/FormDataAttribute.cs b/Vendor/EmbedIO-3.5.2/WebApi/FormDataAttribute.cs new file mode 100644 index 0000000..b5e4a79 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebApi/FormDataAttribute.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Specialized; +using System.Threading.Tasks; + +namespace EmbedIO.WebApi +{ + /// + /// Specifies that a parameter of a controller method will receive a + /// of HTML form data, obtained by deserializing a request body with a content type + /// of application/x-www-form-urlencoded. + /// The received collection will be read-only. + /// This class cannot be inherited. + /// + /// + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class FormDataAttribute : Attribute, IRequestDataAttribute + { + /// + public Task GetRequestDataAsync(WebApiController controller, string parameterName) + => controller.HttpContext.GetRequestFormDataAsync(); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebApi/FormFieldAttribute.cs b/Vendor/EmbedIO-3.5.2/WebApi/FormFieldAttribute.cs new file mode 100644 index 0000000..09bf398 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebApi/FormFieldAttribute.cs @@ -0,0 +1,172 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using EmbedIO.Utilities; +using Swan; + +namespace EmbedIO.WebApi +{ + /// + /// Specifies that a parameter of a controller method will receive the value(s) of a field in a HTML form, + /// obtained by deserializing a request body with a content type of application/x-www-form-urlencoded. + /// The parameter carrying this attribute can be either a simple type or a one-dimension array. + /// If multiple values are present for the field, a non-array parameter will receive the last specified value, + /// while an array parameter will receive an array of field values converted to the element type of the + /// parameter. + /// If a single value is present for the field, a non-array parameter will receive the value converted + /// to the type of the parameter, while an array parameter will receive an array of length 1, containing + /// the value converted to the element type of the parameter + /// If no values are present for the field and the property is + /// , a 400 Bad Request response will be sent to the client, with a message + /// specifying the name of the missing field. + /// If no values are present for the field and the property is + /// , a non-array parameter will receive the default value for its type, while + /// an array parameter will receive an array of length 0. + /// This class cannot be inherited. + /// + /// + /// + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class FormFieldAttribute : + Attribute, + IRequestDataAttribute, + IRequestDataAttribute, + IRequestDataAttribute + { + /// + /// Initializes a new instance of the class. + /// The name of the form field to extract will be equal to the name of the parameter + /// carrying this attribute. + /// + public FormFieldAttribute() + : this(false, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the form field to extract. + /// is . + /// is the empty string (""). + public FormFieldAttribute(string fieldName) + : this(false, Validate.NotNullOrEmpty(nameof(fieldName), fieldName)) + { + } + + /// + /// Initializes a new instance of the class. + /// The name of the form field to extract will be equal to the name of the parameter + /// carrying this attribute. + /// + /// If set to , a 400 Bad Request + /// response will be sent to the client if no values are found for the field; if set to + /// , a default value will be assumed. + public FormFieldAttribute(bool badRequestIfMissing) + : this(badRequestIfMissing, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the form field to extract. + /// is . + /// is the empty string (""). + /// If set to , a 400 Bad Request + /// response will be sent to the client if no values are found for the field; if set to + /// , a default value will be assumed. + public FormFieldAttribute(string fieldName, bool badRequestIfMissing) + : this(badRequestIfMissing, Validate.NotNullOrEmpty(nameof(fieldName), fieldName)) + { + } + + private FormFieldAttribute(bool badRequestIfMissing, string? fieldName) + { + BadRequestIfMissing = badRequestIfMissing; + FieldName = fieldName; + } + + /// + /// Gets the name of the form field that this attribute will extract, + /// or if the name of the parameter carrying this + /// attribute is to be used as field name. + /// + public string? FieldName { get; } + + /// + /// Gets or sets a value indicating whether to send a 400 Bad Request response + /// to the client if the submitted form contains no values for the field. + /// If this property is and the submitted form + /// contains no values for the field, the 400 Bad Request response sent + /// to the client will contain a reference to the missing field. + /// If this property is and the submitted form + /// contains no values for the field, the default value for the parameter + /// (or a zero-length array if the parameter is of an array type) + /// will be passed to the controller method. + /// + public bool BadRequestIfMissing { get; } + + async Task IRequestDataAttribute.GetRequestDataAsync( + WebApiController controller, + string parameterName) + { + var data = await controller.HttpContext.GetRequestFormDataAsync() + .ConfigureAwait(false); + + var fieldName = FieldName ?? parameterName; + if (!data.ContainsKey(fieldName) && BadRequestIfMissing) + throw HttpException.BadRequest($"Missing form field {fieldName}."); + + return data.GetValues(fieldName)?.LastOrDefault(); + } + + async Task IRequestDataAttribute.GetRequestDataAsync( + WebApiController controller, + string parameterName) + { + var data = await controller.HttpContext.GetRequestFormDataAsync() + .ConfigureAwait(false); + + var fieldName = FieldName ?? parameterName; + if (!data.ContainsKey(fieldName) && BadRequestIfMissing) + throw HttpException.BadRequest($"Missing form field {fieldName}."); + + return data.GetValues(fieldName) ?? Array.Empty(); + } + + async Task IRequestDataAttribute.GetRequestDataAsync( + WebApiController controller, + Type type, + string parameterName) + { + var data = await controller.HttpContext.GetRequestFormDataAsync() + .ConfigureAwait(false); + + var fieldName = FieldName ?? parameterName; + if (!data.ContainsKey(fieldName) && BadRequestIfMissing) + throw HttpException.BadRequest($"Missing form field {fieldName}."); + + if (type.IsArray) + { + var fieldValues = data.GetValues(fieldName) ?? Array.Empty(); + if (!FromString.TryConvertTo(type, fieldValues, out var result)) + throw HttpException.BadRequest($"Cannot convert field {fieldName} to an array of {type.GetElementType().Name}."); + + return result; + } + else + { + var fieldValue = data.GetValues(fieldName)?.LastOrDefault(); + if (fieldValue == null) + return type.IsValueType ? Activator.CreateInstance(type) : null; + + if (!FromString.TryConvertTo(type, fieldValue, out var result)) + throw HttpException.BadRequest($"Cannot convert field {fieldName} to {type.Name}."); + + return result; + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebApi/IRequestDataAttribute`1.cs b/Vendor/EmbedIO-3.5.2/WebApi/IRequestDataAttribute`1.cs new file mode 100644 index 0000000..369e3fe --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebApi/IRequestDataAttribute`1.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading.Tasks; + +namespace EmbedIO.WebApi +{ + /// + /// Represents an attribute, applied to a parameter of a web API controller method, + /// that causes the parameter to be passed deserialized data from a request. + /// + /// The type of the controller. + /// + public interface IRequestDataAttribute + where TController : WebApiController + { + /// + /// Asynchronously obtains data from a controller's context. + /// + /// The controller. + /// The type of the parameter that has to receive the data. + /// The name of the parameter that has to receive the data. + /// a whose result will be the data + /// to pass as a parameter to a controller method. + Task GetRequestDataAsync(TController controller, Type type, string parameterName); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebApi/IRequestDataAttribute`2.cs b/Vendor/EmbedIO-3.5.2/WebApi/IRequestDataAttribute`2.cs new file mode 100644 index 0000000..bbfff0e --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebApi/IRequestDataAttribute`2.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; + +namespace EmbedIO.WebApi +{ + /// + /// Represents an attribute, applied to a parameter of a web API controller method, + /// that causes the parameter to be passed deserialized data from a request. + /// + /// The type of the controller. + /// The type of the data. + /// + public interface IRequestDataAttribute + where TController : WebApiController + where TData : class + { + /// + /// Asynchronously obtains data from a controller's context. + /// + /// The controller. + /// The name of the parameter that has to receive the data. + /// a whose result will be the data + /// to pass as a parameter to a controller method. + Task GetRequestDataAsync(TController controller, string parameterName); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebApi/JsonDataAttribute.cs b/Vendor/EmbedIO-3.5.2/WebApi/JsonDataAttribute.cs new file mode 100644 index 0000000..9d31c44 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebApi/JsonDataAttribute.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; + +namespace EmbedIO.WebApi +{ + /// + /// Specifies that a parameter of a controller method will receive + /// an object obtained by deserializing the request body as JSON. + /// The received object will be + /// only if the deserialized object is null. + /// If the request body is not valid JSON, + /// or if it cannot be deserialized to the type of the parameter, + /// a 400 Bad Request response will be sent to the client. + /// This class cannot be inherited. + /// + /// + /// + [AttributeUsage(AttributeTargets.Parameter)] + public class JsonDataAttribute : Attribute, IRequestDataAttribute + { + /// + public async Task GetRequestDataAsync(WebApiController controller, Type type, string parameterName) + { + string body; + using (var reader = controller.HttpContext.OpenRequestText()) + { + body = await reader.ReadToEndAsync().ConfigureAwait(false); + } + + try + { + return Swan.Formatters.Json.Deserialize(body, type); + } + catch (FormatException) + { + throw HttpException.BadRequest($"Expected request body to be deserializable to {type.FullName}."); + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebApi/QueryDataAttribute.cs b/Vendor/EmbedIO-3.5.2/WebApi/QueryDataAttribute.cs new file mode 100644 index 0000000..4990be9 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebApi/QueryDataAttribute.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Specialized; +using System.Threading.Tasks; + +namespace EmbedIO.WebApi +{ + /// + /// Specifies that a parameter of a controller method will receive a + /// of HTML form data, obtained by deserializing a request URL query. + /// The received collection will be read-only. + /// This class cannot be inherited. + /// + /// + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class QueryDataAttribute : Attribute, IRequestDataAttribute + { + /// + public Task GetRequestDataAsync(WebApiController controller, string parameterName) + => Task.FromResult(controller.HttpContext.GetRequestQueryData()); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebApi/QueryFieldAttribute.cs b/Vendor/EmbedIO-3.5.2/WebApi/QueryFieldAttribute.cs new file mode 100644 index 0000000..efbd1cf --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebApi/QueryFieldAttribute.cs @@ -0,0 +1,169 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using EmbedIO.Utilities; +using Swan; + +namespace EmbedIO.WebApi +{ + /// + /// Specifies that a parameter of a controller method will receive the value of a field, + /// obtained by deserializing a request URL query. + /// The parameter carrying this attribute can be either a simple type or a one-dimension array. + /// If multiple values are present for the field, a non-array parameter will receive the last specified value, + /// while an array parameter will receive an array of field values converted to the element type of the + /// parameter. + /// If a single value is present for the field, a non-array parameter will receive the value converted + /// to the type of the parameter, while an array parameter will receive an array of length 1, containing + /// the value converted to the element type of the parameter + /// If no values are present for the field and the property is + /// , a 400 Bad Request response will be sent to the client, with a message + /// specifying the name of the missing field. + /// If no values are present for the field and the property is + /// , a non-array parameter will receive the default value for its type, while + /// an array parameter will receive an array of length 0. + /// This class cannot be inherited. + /// + /// + /// + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class QueryFieldAttribute : + Attribute, + IRequestDataAttribute, + IRequestDataAttribute, + IRequestDataAttribute + { + /// + /// Initializes a new instance of the class. + /// The name of the query field to extract will be equal to the name of the parameter + /// carrying this attribute. + /// + public QueryFieldAttribute() + : this(false, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the query field to extract. + /// is . + /// is the empty string (""). + public QueryFieldAttribute(string fieldName) + : this(false, Validate.NotNullOrEmpty(nameof(fieldName), fieldName)) + { + } + + /// + /// Initializes a new instance of the class. + /// The name of the query field to extract will be equal to the name of the parameter + /// carrying this attribute. + /// + /// If set to , a 400 Bad Request + /// response will be sent to the client if no values are found for the field; if set to + /// , a default value will be assumed. + public QueryFieldAttribute(bool badRequestIfMissing) + : this(badRequestIfMissing, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the query field to extract. + /// is . + /// is the empty string (""). + /// If set to , a 400 Bad Request + /// response will be sent to the client if no values are found for the field; if set to + /// , a default value will be assumed. + public QueryFieldAttribute(string fieldName, bool badRequestIfMissing) + : this(badRequestIfMissing, Validate.NotNullOrEmpty(nameof(fieldName), fieldName)) + { + } + + private QueryFieldAttribute(bool badRequestIfMissing, string? fieldName) + { + BadRequestIfMissing = badRequestIfMissing; + FieldName = fieldName; + } + + /// + /// Gets the name of the query field that this attribute will extract, + /// or if the name of the parameter carrying this + /// attribute is to be used as field name. + /// + public string? FieldName { get; } + + /// + /// Gets or sets a value indicating whether to send a 400 Bad Request response + /// to the client if the URL query contains no values for the field. + /// If this property is and the URL query + /// contains no values for the field, the 400 Bad Request response sent + /// to the client will contain a reference to the missing field. + /// If this property is and the URL query + /// contains no values for the field, the default value for the parameter + /// (or a zero-length array if the parameter is of an array type) + /// will be passed to the controller method. + /// + public bool BadRequestIfMissing { get; } + + Task IRequestDataAttribute.GetRequestDataAsync( + WebApiController controller, + string parameterName) + { + var data = controller.HttpContext.GetRequestQueryData(); + + var fieldName = FieldName ?? parameterName; + if (!data.ContainsKey(fieldName) && BadRequestIfMissing) + throw HttpException.BadRequest($"Missing query field {fieldName}."); + + return Task.FromResult(data.GetValues(fieldName)?.LastOrDefault()); + } + + Task IRequestDataAttribute.GetRequestDataAsync( + WebApiController controller, + string parameterName) + { + var data = controller.HttpContext.GetRequestQueryData(); + + var fieldName = FieldName ?? parameterName; + if (!data.ContainsKey(fieldName) && BadRequestIfMissing) + throw HttpException.BadRequest($"Missing query field {fieldName}."); + + return Task.FromResult(data.GetValues(fieldName) ?? Array.Empty()); + } + + Task IRequestDataAttribute.GetRequestDataAsync( + WebApiController controller, + Type type, + string parameterName) + { + var data = controller.HttpContext.GetRequestQueryData(); + + var fieldName = FieldName ?? parameterName; + if (!data.ContainsKey(fieldName) && BadRequestIfMissing) + throw HttpException.BadRequest($"Missing query field {fieldName}."); + + if (type.IsArray) + { + var fieldValues = data.GetValues(fieldName) ?? Array.Empty(); + if (!FromString.TryConvertTo(type, fieldValues, out var result)) + throw HttpException.BadRequest($"Cannot convert field {fieldName} to an array of {type.GetElementType().Name}."); + + return Task.FromResult(result); + } + else + { + var fieldValue = data.GetValues(fieldName)?.LastOrDefault(); + if (fieldValue == null) + return Task.FromResult(type.IsValueType ? Activator.CreateInstance(type) : null); + + if (!FromString.TryConvertTo(type, fieldValue, out var result)) + throw HttpException.BadRequest($"Cannot convert field {fieldName} to {type.Name}."); + + return Task.FromResult(result); + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebApi/WebApiController.cs b/Vendor/EmbedIO-3.5.2/WebApi/WebApiController.cs new file mode 100644 index 0000000..41a1331 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebApi/WebApiController.cs @@ -0,0 +1,73 @@ +using System.Security.Principal; +using System.Threading; +using EmbedIO.Routing; +using EmbedIO.Sessions; + +namespace EmbedIO.WebApi +{ + /// + /// Inherit from this class and define your own Web API methods + /// You must RegisterController in the Web API Module to make it active. + /// + public abstract class WebApiController + { +// The HttpContext and Route properties are always initialized to non-null values, +// but it's done after creation by a runtime-compiled lambda, +// which the compiler cannot know about, hence the warnings. +#pragma warning disable CS8618 // Non-nullable property is uninitialized. Consider declaring the property as nullable. + + /// + /// Gets the HTTP context. + /// This property is automatically initialized upon controller creation. + /// + public IHttpContext HttpContext { get; internal set; } + + /// + /// Gets the resolved route. + /// This property is automatically initialized upon controller creation. + /// + public RouteMatch Route { get; internal set; } + +#pragma warning restore CS8618 + + /// + /// Gets the used to cancel processing of the request. + /// + public CancellationToken CancellationToken => HttpContext.CancellationToken; + + /// + /// Gets the HTTP request. + /// + public IHttpRequest Request => HttpContext.Request; + + /// + /// Gets the HTTP response object. + /// + public IHttpResponse Response => HttpContext.Response; + + /// + /// Gets the user. + /// + public IPrincipal? User => HttpContext.User; + + /// + /// Gets the session proxy associated with the HTTP context. + /// + public ISessionProxy Session => HttpContext.Session; + + /// + /// This method is meant to be called internally by EmbedIO. + /// Derived classes can override the method + /// to perform common operations before any handler gets called. + /// + /// + public void PreProcessRequest() => OnBeforeHandler(); + + /// + /// Called before a handler to perform common operations. + /// The default behavior is to set response headers + /// in order to prevent caching of the response. + /// + protected virtual void OnBeforeHandler() => HttpContext.Response.DisableCaching(); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebApi/WebApiModule.cs b/Vendor/EmbedIO-3.5.2/WebApi/WebApiModule.cs new file mode 100644 index 0000000..23d5c7d --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebApi/WebApiModule.cs @@ -0,0 +1,94 @@ +using System; +using System.Threading.Tasks; +using EmbedIO.Utilities; + +namespace EmbedIO.WebApi +{ + /// + /// A module using class methods as handlers. + /// Public instance methods that match the WebServerModule.ResponseHandler signature, and have the WebApi handler attribute + /// will be used to respond to web server requests. + /// + public class WebApiModule : WebApiModuleBase + { + /// + /// Initializes a new instance of the class, + /// using the default response serializer. + /// + /// The base URL path served by this module. + /// + /// + public WebApiModule(string baseRoute) + : base(baseRoute) + { + } + + /// + /// Initializes a new instance of the class, + /// using the specified response serializer. + /// + /// The base URL path served by this module. + /// A used to serialize + /// the result of controller methods returning + /// or Task<object>. + /// is . + /// + /// + public WebApiModule(string baseRoute, ResponseSerializerCallback serializer) + : base(baseRoute, serializer) + { + } + + /// + /// Registers a controller type using a constructor. + /// See + /// for further information. + /// + /// The type of the controller. + /// + /// + /// + public void RegisterController() + where TController : WebApiController, new() + => RegisterControllerType(typeof(TController)); + + /// + /// Registers a controller type using a factory method. + /// See + /// for further information. + /// + /// The type of the controller. + /// The factory method used to construct instances of . + /// + /// + /// + public void RegisterController(Func factory) + where TController : WebApiController + => RegisterControllerType(typeof(TController), factory); + + /// + /// Registers a controller type using a constructor. + /// See + /// for further information. + /// + /// The type of the controller. + /// + /// + /// + public void RegisterController(Type controllerType) + => RegisterControllerType(controllerType); + + /// + /// Registers a controller type using a factory method. + /// See + /// for further information. + /// + /// The type of the controller. + /// The factory method used to construct instances of . + /// + /// + /// + public void RegisterController(Type controllerType, Func factory) + => RegisterControllerType(controllerType, factory); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebApi/WebApiModuleBase.cs b/Vendor/EmbedIO-3.5.2/WebApi/WebApiModuleBase.cs new file mode 100644 index 0000000..118dee7 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebApi/WebApiModuleBase.cs @@ -0,0 +1,600 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using EmbedIO.Routing; +using EmbedIO.Utilities; +using Swan; + +namespace EmbedIO.WebApi +{ + /// + /// A module using objects derived from + /// as collections of handler methods. + /// + public abstract class WebApiModuleBase : RoutingModuleBase + { + private const string GetRequestDataAsyncMethodName = nameof(IRequestDataAttribute.GetRequestDataAsync); + + private static readonly MethodInfo PreProcessRequestMethod = typeof(WebApiController).GetMethod(nameof(WebApiController.PreProcessRequest)); + private static readonly MethodInfo HttpContextSetter = typeof(WebApiController).GetProperty(nameof(WebApiController.HttpContext)).GetSetMethod(true); + private static readonly MethodInfo RouteSetter = typeof(WebApiController).GetProperty(nameof(WebApiController.Route)).GetSetMethod(true); + private static readonly MethodInfo AwaitResultMethod = typeof(WebApiModuleBase).GetMethod(nameof(AwaitResult), BindingFlags.Static | BindingFlags.NonPublic); + private static readonly MethodInfo AwaitAndCastResultMethod = typeof(WebApiModuleBase).GetMethod(nameof(AwaitAndCastResult), BindingFlags.Static | BindingFlags.NonPublic); + private static readonly MethodInfo DisposeMethod = typeof(IDisposable).GetMethod(nameof(IDisposable.Dispose)); + private static readonly MethodInfo SerializeResultAsyncMethod = typeof(WebApiModuleBase).GetMethod(nameof(SerializeResultAsync), BindingFlags.Instance | BindingFlags.NonPublic); + + private readonly HashSet _controllerTypes = new HashSet(); + + /// + /// Initializes a new instance of the class, + /// using the default response serializer. + /// + /// The base route served by this module. + /// + /// + protected WebApiModuleBase(string baseRoute) + : this(baseRoute, ResponseSerializer.Default) + { + } + + /// + /// Initializes a new instance of the class, + /// using the specified response serializer. + /// + /// The base route served by this module. + /// A used to serialize + /// the result of controller methods returning + /// or Task<object>. + /// is . + /// + /// + protected WebApiModuleBase(string baseRoute, ResponseSerializerCallback serializer) + : base(baseRoute) + { + Serializer = Validate.NotNull(nameof(serializer), serializer); + } + + /// + /// A used to serialize + /// the result of controller methods returning values. + /// + public ResponseSerializerCallback Serializer { get; } + + /// + /// Gets the number of controller types registered in this module. + /// + public int ControllerCount => _controllerTypes.Count; + + /// + /// Registers a controller type using a constructor. + /// In order for registration to be successful, the specified controller type: + /// + /// must be a subclass of ; + /// must not be an abstract class; + /// must not be a generic type definition; + /// must have a public parameterless constructor. + /// + /// + /// The type of the controller. + /// The module's configuration is locked. + /// + /// is already registered in this module. + /// does not satisfy the prerequisites + /// listed in the Summary section. + /// + /// + /// A new instance of will be created + /// for each request to handle, and dereferenced immediately afterwards, + /// to be collected during next garbage collection cycle. + /// is not required to be thread-safe, + /// as it will be constructed and used in the same synchronization context. + /// However, since request handling is asynchronous, the actual execution thread + /// may vary during execution. Care must be exercised when using thread-sensitive + /// resources or thread-static data. + /// If implements , + /// its Dispose method will be called when it has + /// finished handling a request. + /// + /// + /// + protected void RegisterControllerType() + where TController : WebApiController, new() + => RegisterControllerType(typeof(TController)); + + /// + /// Registers a controller type using a factory method. + /// In order for registration to be successful: + /// + /// must be a subclass of ; + /// must not be a generic type definition; + /// 's return type must be either + /// or a subclass of . + /// + /// + /// The type of the controller. + /// The factory method used to construct instances of . + /// The module's configuration is locked. + /// is . + /// + /// is already registered in this module. + /// - or - + /// does not satisfy the prerequisites listed in the Summary section. + /// + /// + /// will be called once for each request to handle + /// in order to obtain an instance of . + /// The returned instance will be dereferenced immediately after handling the request. + /// is not required to be thread-safe, + /// as it will be constructed and used in the same synchronization context. + /// However, since request handling is asynchronous, the actual execution thread + /// may vary during execution. Care must be exercised when using thread-sensitive + /// resources or thread-static data. + /// If implements , + /// its Dispose method will be called when it has + /// finished handling a request. In this case it is recommended that + /// return a newly-constructed instance of + /// at each invocation. + /// If does not implement , + /// may employ techniques such as instance pooling to avoid + /// the overhead of constructing a new instance of + /// at each invocation. If so, resources such as file handles, database connections, etc. + /// should be freed before returning from each handler method to avoid + /// starvation. + /// + /// + /// + protected void RegisterControllerType(Func factory) + where TController : WebApiController + => RegisterControllerType(typeof(TController), factory); + + /// + /// Registers a controller type using a constructor. + /// In order for registration to be successful, the specified : + /// + /// must be a subclass of ; + /// must not be an abstract class; + /// must not be a generic type definition; + /// must have a public parameterless constructor. + /// + /// + /// The type of the controller. + /// The module's configuration is locked. + /// is . + /// + /// is already registered in this module. + /// - or - + /// does not satisfy the prerequisites + /// listed in the Summary section. + /// + /// + /// A new instance of will be created + /// for each request to handle, and dereferenced immediately afterwards, + /// to be collected during next garbage collection cycle. + /// is not required to be thread-safe, + /// as it will be constructed and used in the same synchronization context. + /// However, since request handling is asynchronous, the actual execution thread + /// may vary during execution. Care must be exercised when using thread-sensitive + /// resources or thread-static data. + /// If implements , + /// its Dispose method will be called when it has + /// finished handling a request. + /// + /// + /// + protected void RegisterControllerType(Type controllerType) + { + EnsureConfigurationNotLocked(); + + controllerType = ValidateControllerType(nameof(controllerType), controllerType, false); + + var constructor = controllerType.GetConstructors().FirstOrDefault(c => c.GetParameters().Length == 0); + if (constructor == null) + { + throw new ArgumentException( + "Controller type must have a public parameterless constructor.", + nameof(controllerType)); + } + + if (!TryRegisterControllerTypeCore(controllerType, Expression.New(constructor))) + throw new ArgumentException($"Type {controllerType.Name} contains no controller methods."); + } + + /// + /// Registers a controller type using a factory method. + /// In order for registration to be successful: + /// + /// must be a subclass of ; + /// must not be a generic type definition; + /// 's return type must be either + /// or a subclass of . + /// + /// + /// The type of the controller. + /// The factory method used to construct instances of . + /// The module's configuration is locked. + /// + /// is . + /// - or - + /// is . + /// + /// + /// is already registered in this module. + /// - or - + /// One or more parameters do not satisfy the prerequisites listed in the Summary section. + /// + /// + /// will be called once for each request to handle + /// in order to obtain an instance of . + /// The returned instance will be dereferenced immediately after handling the request. + /// is not required to be thread-safe, + /// as it will be constructed and used in the same synchronization context. + /// However, since request handling is asynchronous, the actual execution thread + /// may vary during execution. Care must be exercised when using thread-sensitive + /// resources or thread-static data. + /// If implements , + /// its Dispose method will be called when it has + /// finished handling a request. In this case it is recommended that + /// return a newly-constructed instance of + /// at each invocation. + /// If does not implement , + /// may employ techniques such as instance pooling to avoid + /// the overhead of constructing a new instance of + /// at each invocation. If so, resources such as file handles, database connections, etc. + /// should be freed before returning from each handler method to avoid + /// starvation. + /// + /// + /// + protected void RegisterControllerType(Type controllerType, Func factory) + { + EnsureConfigurationNotLocked(); + + controllerType = ValidateControllerType(nameof(controllerType), controllerType, true); + factory = Validate.NotNull(nameof(factory), factory); + if (!controllerType.IsAssignableFrom(factory.Method.ReturnType)) + throw new ArgumentException("Factory method has an incorrect return type.", nameof(factory)); + + var expression = Expression.Call( + factory.Target == null ? null : Expression.Constant(factory.Target), + factory.Method); + + if (!TryRegisterControllerTypeCore(controllerType, expression)) + throw new ArgumentException($"Type {controllerType.Name} contains no controller methods."); + } + + private static int IndexOfRouteParameter(RouteMatcher matcher, string name) + { + var names = matcher.ParameterNames; + for (var i = 0; i < names.Count; i++) + { + if (names[i] == name) + return i; + } + + return -1; + } + + private static T AwaitResult(Task task) => task.ConfigureAwait(false).GetAwaiter().GetResult(); + + private static T AwaitAndCastResult(string parameterName, Task task) + { + var result = task.ConfigureAwait(false).GetAwaiter().GetResult(); + + return result switch { + null when typeof(T).IsValueType && Nullable.GetUnderlyingType(typeof(T)) == null => throw new InvalidCastException($"Cannot cast null to {typeof(T).FullName} for parameter \"{parameterName}\"."), + null => default, + T castResult => castResult, + _ => throw new InvalidCastException($"Cannot cast {result.GetType().FullName} to {typeof(T).FullName} for parameter \"{parameterName}\".") + }; + } + + private static bool IsGenericTaskType(Type type, out Type? resultType) + { + resultType = null; + + if (!type.IsConstructedGenericType) + return false; + + if (type.GetGenericTypeDefinition() != typeof(Task<>)) + return false; + + resultType = type.GetGenericArguments()[0]; + return true; + } + + // Compile a handler. + // + // Parameters: + // - factoryExpression is an Expression that builds a controller; + // - method is a MethodInfo for a public instance method of the controller; + // - route is the route to which the controller method is associated. + // + // This method builds a lambda, with the same signature as a RouteHandlerCallback, that: + // - uses factoryExpression to build a controller; + // - calls the controller method, passing converted route parameters for method parameters with matching names + // and default values for other parameters; + // - serializes the returned object (or the result of the returned task), + // unless the return type of the controller method is void or Task; + // - if the controller implements IDisposable, disposes it. + private RouteHandlerCallback CompileHandler(Expression factoryExpression, MethodInfo method, RouteMatcher matcher) + { + // Lambda parameters + var contextInLambda = Expression.Parameter(typeof(IHttpContext), "context"); + var routeInLambda = Expression.Parameter(typeof(RouteMatch), "route"); + + // Local variables + var locals = new List(); + + // Local variable for controller + var controllerType = method.ReflectedType; + var controller = Expression.Variable(controllerType, "controller"); + locals.Add(controller); + + // Label for return statement + var returnTarget = Expression.Label(typeof(Task)); + + // Contents of lambda body + var bodyContents = new List(); + + // Build lambda arguments + var parameters = method.GetParameters(); + var parameterCount = parameters.Length; + var handlerArguments = new List(); + for (var i = 0; i < parameterCount; i++) + { + var parameter = parameters[i]; + var parameterType = parameter.ParameterType; + var failedToUseRequestDataAttributes = false; + + // First, check for generic request data interfaces in attributes + var requestDataInterfaces = parameter.GetCustomAttributes() + .Aggregate(new List<(Attribute Attr, Type Intf)>(), (list, attr) => { + list.AddRange(attr.GetType().GetInterfaces() + .Where(x => x.IsConstructedGenericType + && x.GetGenericTypeDefinition() == typeof(IRequestDataAttribute<,>)) + .Select(x => (attr, x))); + + return list; + }); + + // If there are any... + if (requestDataInterfaces.Count > 0) + { + // Take the first that applies to both controller and parameter type + var (attr, intf) = requestDataInterfaces.FirstOrDefault( + x => x.Intf.GenericTypeArguments[0].IsAssignableFrom(controllerType) + && parameterType.IsAssignableFrom(x.Intf.GenericTypeArguments[1])); + + if (attr != null) + { + // Use the request data interface to get a value for the parameter. + Expression useRequestDataInterface = Expression.Call( + Expression.Constant(attr), + intf.GetMethod(GetRequestDataAsyncMethodName), + controller, + Expression.Constant(parameter.Name)); + + // We should await the call to GetRequestDataAsync. + // For lack of a better way, call AwaitResult with an appropriate type argument. + useRequestDataInterface = Expression.Call( + AwaitResultMethod.MakeGenericMethod(intf.GenericTypeArguments[1]), + useRequestDataInterface); + + handlerArguments.Add(useRequestDataInterface); + continue; + } + + // If there is no interface to use, the user expects data to be injected + // but provided no way of injecting the right data type. + failedToUseRequestDataAttributes = true; + } + + // Check for non-generic request data interfaces in attributes + requestDataInterfaces = parameter.GetCustomAttributes() + .Aggregate(new List<(Attribute Attr, Type Intf)>(), (list, attr) => { + list.AddRange(attr.GetType().GetInterfaces() + .Where(x => x.IsConstructedGenericType + && x.GetGenericTypeDefinition() == typeof(IRequestDataAttribute<>)) + .Select(x => (attr, x))); + + return list; + }); + + // If there are any... + if (requestDataInterfaces.Count > 0) + { + // Take the first that applies to the controller + var (attr, intf) = requestDataInterfaces.FirstOrDefault( + x => x.Intf.GenericTypeArguments[0].IsAssignableFrom(controllerType)); + + if (attr != null) + { + // Use the request data interface to get a value for the parameter. + Expression useRequestDataInterface = Expression.Call( + Expression.Constant(attr), + intf.GetMethod(GetRequestDataAsyncMethodName), + controller, + Expression.Constant(parameterType), + Expression.Constant(parameter.Name)); + + // We should await the call to GetRequestDataAsync, + // then cast the result to the parameter type. + // For lack of a better way to do the former, + // and to save one function call, + // just call AwaitAndCastResult with an appropriate type argument. + useRequestDataInterface = Expression.Call( + AwaitAndCastResultMethod.MakeGenericMethod(parameterType), + Expression.Constant(parameter.Name), + useRequestDataInterface); + + handlerArguments.Add(useRequestDataInterface); + continue; + } + + // If there is no interface to use, the user expects data to be injected + // but provided no way of injecting the right data type. + failedToUseRequestDataAttributes = true; + } + + // There are request data attributes, but none is suitable + // for the type of the parameter. + if (failedToUseRequestDataAttributes) + throw new InvalidOperationException($"No request data attribute for parameter {parameter.Name} of method {controllerType.Name}.{method.Name} can provide the expected data type."); + + // Check whether the name of the handler parameter matches the name of a route parameter. + var index = IndexOfRouteParameter(matcher, parameter.Name); + if (index >= 0) + { + // Convert the parameter to the handler's parameter type. + var convertFromRoute = FromString.ConvertExpressionTo( + parameterType, + Expression.Property(routeInLambda, "Item", Expression.Constant(index))); + + handlerArguments.Add(convertFromRoute ?? throw SelfCheck.Failure($"{nameof(convertFromRoute)} is null.")); + continue; + } + + // No route parameter has the same name as a handler parameter. + // Pass the default for the parameter type. + handlerArguments.Add(parameter.HasDefaultValue + ? (Expression)Expression.Constant(parameter.DefaultValue) + : Expression.Default(parameterType)); + } + + // Create the controller and initialize its properties + bodyContents.Add(Expression.Assign(controller,factoryExpression)); + bodyContents.Add(Expression.Call(controller, HttpContextSetter, contextInLambda)); + bodyContents.Add(Expression.Call(controller, RouteSetter, routeInLambda)); + + // Build the handler method call + Expression callMethod = Expression.Call(controller, method, handlerArguments); + var methodReturnType = method.ReturnType; + if (methodReturnType == typeof(Task)) + { + // Nothing to do + } + else if (methodReturnType == typeof(void)) + { + // Convert void to Task by evaluating Task.CompletedTask + callMethod = Expression.Block(typeof(Task), callMethod, Expression.Constant(Task.CompletedTask)); + } + else if (IsGenericTaskType(methodReturnType, out var resultType)) + { + // Return a Task that serializes the result of a Task + callMethod = Expression.Call( + Expression.Constant(this), + SerializeResultAsyncMethod.MakeGenericMethod(resultType), + contextInLambda, + callMethod); + } + else + { + // Return a Task that serializes a result obtained synchronously + callMethod = Expression.Call( + Serializer.Target == null ? null : Expression.Constant(Serializer.Target), + Serializer.Method, + contextInLambda, + Expression.Convert(callMethod, typeof(object))); + } + + // Operations to perform on the controller. + // Pseudocode: + // controller.PreProcessRequest(); + // return controller.method(handlerArguments); + Expression workWithController = Expression.Block( + Expression.Call(controller, PreProcessRequestMethod), + Expression.Return(returnTarget, callMethod)); + + // If the controller type implements IDisposable, + // wrap operations in a simulated using block. + if (typeof(IDisposable).IsAssignableFrom(controllerType)) + { + // Pseudocode: + // try + // { + // body(); + // } + // finally + // { + // (controller as IDisposable).Dispose(); + // } + workWithController = Expression.TryFinally( + workWithController, + Expression.Call(Expression.TypeAs(controller, typeof(IDisposable)), DisposeMethod)); + } + + bodyContents.Add(workWithController); + + // At the end of the lambda body is the target of return statements. + bodyContents.Add(Expression.Label(returnTarget, Expression.Constant(Task.FromResult(false)))); + + // Build and compile the lambda. + return Expression.Lambda( + Expression.Block(locals, bodyContents), + contextInLambda, + routeInLambda) + .Compile(); + } + + private async Task SerializeResultAsync(IHttpContext context, Task task) + { + await Serializer( + context, + await task.ConfigureAwait(false)).ConfigureAwait(false); + } + + private Type ValidateControllerType(string argumentName, Type value, bool canBeAbstract) + { + value = Validate.NotNull(argumentName, value); + if (canBeAbstract) + { + if (value.IsGenericTypeDefinition + || !value.IsSubclassOf(typeof(WebApiController))) + throw new ArgumentException($"Controller type must be a subclass of {nameof(WebApiController)}.", argumentName); + } + else + { + if (value.IsAbstract + || value.IsGenericTypeDefinition + || !value.IsSubclassOf(typeof(WebApiController))) + throw new ArgumentException($"Controller type must be a non-abstract subclass of {nameof(WebApiController)}.", argumentName); + } + + if (_controllerTypes.Contains(value)) + throw new ArgumentException("Controller type is already registered in this module.", argumentName); + + return value; + } + + private bool TryRegisterControllerTypeCore(Type controllerType, Expression factoryExpression) + { + var handlerCount = 0; + var methods = controllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public) + .Where(m => !m.ContainsGenericParameters); + + foreach (var method in methods) + { + var attributes = method.GetCustomAttributes() + .OfType() + .ToArray(); + if (attributes.Length < 1) + continue; + + foreach (var attribute in attributes) + { + AddHandler(attribute.Verb, attribute.Matcher, CompileHandler(factoryExpression, method, attribute.Matcher)); + handlerCount++; + } + } + + if (handlerCount < 1) + return false; + + _controllerTypes.Add(controllerType); + return true; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebApi/WebApiModuleExtensions.cs b/Vendor/EmbedIO-3.5.2/WebApi/WebApiModuleExtensions.cs new file mode 100644 index 0000000..c63b3b8 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebApi/WebApiModuleExtensions.cs @@ -0,0 +1,82 @@ +using System; + +namespace EmbedIO.WebApi +{ + /// + /// Provides extension methods for . + /// + public static class WebApiModuleExtensions + { + /// + /// Registers a controller type using a constructor. + /// See + /// for further information. + /// + /// The type of the controller. + /// The on which this method is called. + /// with the controller type registered. + /// + /// + /// + public static WebApiModule WithController(this WebApiModule @this) + where TController : WebApiController, new() + { + @this.RegisterController(); + return @this; + } + + /// + /// Registers a controller type using a factory method. + /// See + /// for further information. + /// + /// The type of the controller. + /// The on which this method is called. + /// The factory method used to construct instances of . + /// with the controller type registered. + /// + /// + /// + public static WebApiModule WithController(this WebApiModule @this, Func factory) + where TController : WebApiController + { + @this.RegisterController(factory); + return @this; + } + + /// + /// Registers a controller type using a constructor. + /// See + /// for further information. + /// + /// The on which this method is called. + /// The type of the controller. + /// with the controller type registered. + /// + /// + /// + public static WebApiModule WithController(this WebApiModule @this, Type controllerType) + { + @this.RegisterController(controllerType); + return @this; + } + + /// + /// Registers a controller type using a factory method. + /// See + /// for further information. + /// + /// The on which this method is called. + /// The type of the controller. + /// The factory method used to construct instances of . + /// with the controller type registered. + /// + /// + /// + public static WebApiModule WithController(this WebApiModule @this, Type controllerType, Func factory) + { + @this.RegisterController(controllerType, factory); + return @this; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebModuleBase.cs b/Vendor/EmbedIO-3.5.2/WebModuleBase.cs new file mode 100644 index 0000000..ccfefbd --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebModuleBase.cs @@ -0,0 +1,153 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Internal; +using EmbedIO.Routing; +using EmbedIO.Utilities; +using Swan.Configuration; + +namespace EmbedIO +{ + /// + /// Base class to define web modules. + /// Although it is not required that a module inherits from this class, + /// it provides some useful features: + /// + /// validation and immutability of the property, + /// which are of paramount importance for the correct functioning of a web server; + /// support for configuration locking upon web server startup + /// (see the property + /// and the method); + /// a basic implementation of the method + /// for modules that do not need to do anything upon web server startup; + /// implementation of the callback property. + /// + /// + public abstract class WebModuleBase : ConfiguredObject, IWebModule + { + private readonly RouteMatcher _routeMatcher; + + private ExceptionHandlerCallback? _onUnhandledException; + private HttpExceptionHandlerCallback? _onHttpException; + + /// + /// Initializes a new instance of the class. + /// + /// The base route served by this module. + /// is . + /// is not a valid base route. + /// + /// + protected WebModuleBase(string baseRoute) + { + BaseRoute = Validate.Route(nameof(baseRoute), baseRoute, true); + _routeMatcher = RouteMatcher.Parse(baseRoute, true); + LogSource = GetType().Name; + } + + /// + public string BaseRoute { get; } + + /// + /// The module's configuration is locked. + public ExceptionHandlerCallback? OnUnhandledException + { + get => _onUnhandledException; + set + { + EnsureConfigurationNotLocked(); + _onUnhandledException = value; + } + } + + /// + /// The module's configuration is locked. + public HttpExceptionHandlerCallback? OnHttpException + { + get => _onHttpException; + set + { + EnsureConfigurationNotLocked(); + _onHttpException = value; + } + } + + /// + public abstract bool IsFinalHandler { get; } + + /// + /// Gets a string to use as a source for log messages. + /// + protected string LogSource { get; } + + /// + /// + /// The module's configuration is locked before returning from this method. + /// + public void Start(CancellationToken cancellationToken) + { + OnStart(cancellationToken); + LockConfiguration(); + } + + /// + public RouteMatch? MatchUrlPath(string urlPath) => _routeMatcher.Match(urlPath); + + /// + public async Task HandleRequestAsync(IHttpContext context) + { + var contextImpl = context.GetImplementation(); + var mimeTypeProvider = this as IMimeTypeProvider; + if (mimeTypeProvider != null) + contextImpl?.MimeTypeProviders.Push(mimeTypeProvider); + + try + { + await OnRequestAsync(context).ConfigureAwait(false); + if (IsFinalHandler) + context.SetHandled(); + } + catch (RequestHandlerPassThroughException) + { } + catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) + { + throw; // Let the web server handle it + } + catch (HttpListenerException) + { + throw; // Let the web server handle it + } + catch (Exception exception) when (exception is IHttpException) + { + await HttpExceptionHandler.Handle(LogSource, context, exception, _onHttpException) + .ConfigureAwait(false); + } + catch (Exception exception) + { + await ExceptionHandler.Handle(LogSource, context, exception, _onUnhandledException, _onHttpException) + .ConfigureAwait(false); + } + finally + { + if (mimeTypeProvider != null) + contextImpl?.MimeTypeProviders.Pop(); + } + } + + /// + /// Called to handle a request from a client. + /// + /// The context of the request being handled. + /// A representing the ongoing operation. + protected abstract Task OnRequestAsync(IHttpContext context); + + /// + /// Called when a module is started, immediately before locking the module's configuration. + /// + /// A used to stop the web server. + protected virtual void OnStart(CancellationToken cancellationToken) + { + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebModuleContainer.cs b/Vendor/EmbedIO-3.5.2/WebModuleContainer.cs new file mode 100644 index 0000000..16bdabc --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebModuleContainer.cs @@ -0,0 +1,18 @@ +using EmbedIO.Internal; + +namespace EmbedIO +{ + /// + /// Provides useful constants for dealing with module containers. + /// + public static class WebModuleContainer + { + /// + /// Gets an interface that does not and cannot contain + /// any module. + /// This field is useful to initialize non-nullable fields or properties + /// of type . + /// + public static readonly IWebModuleContainer None = DummyWebModuleContainer.Instance; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-Actions.cs b/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-Actions.cs new file mode 100644 index 0000000..e1295dd --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-Actions.cs @@ -0,0 +1,311 @@ +using System; +using EmbedIO.Actions; +using EmbedIO.Utilities; +using Swan; + +namespace EmbedIO +{ + partial class WebModuleContainerExtensions + { + /// + /// Creates an instance of and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// The HTTP verb that will be served by . + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer WithAction(this TContainer @this, string baseRoute, HttpVerbs verb, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + { + @this.Modules.Add(new ActionModule(baseRoute, verb, handler)); + return @this; + } + + /// + /// Creates an instance of with a base URL path of "/" + /// and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The HTTP verb that will be served by . + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer WithAction(this TContainer @this, HttpVerbs verb, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, UrlPath.Root, verb, handler); + + /// + /// Creates an instance of that intercepts all requests + /// under the specified and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnAny(this TContainer @this, string baseRoute, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, baseRoute, HttpVerbs.Any, handler); + + /// + /// Creates an instance of that intercepts all requests + /// and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnAny(this TContainer @this, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, UrlPath.Root, HttpVerbs.Any, handler); + + /// + /// Creates an instance of that intercepts all DELETErequests + /// under the specified and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnDelete(this TContainer @this, string baseRoute, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, baseRoute, HttpVerbs.Delete, handler); + + /// + /// Creates an instance of that intercepts all DELETErequests + /// and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnDelete(this TContainer @this, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, UrlPath.Root, HttpVerbs.Delete, handler); + + /// + /// Creates an instance of that intercepts all GETrequests + /// under the specified and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnGet(this TContainer @this, string baseRoute, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, baseRoute, HttpVerbs.Get, handler); + + /// + /// Creates an instance of that intercepts all GETrequests + /// and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnGet(this TContainer @this, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, UrlPath.Root, HttpVerbs.Get, handler); + + /// + /// Creates an instance of that intercepts all HEADrequests + /// under the specified and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnHead(this TContainer @this, string baseRoute, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, baseRoute, HttpVerbs.Head, handler); + + /// + /// Creates an instance of that intercepts all HEADrequests + /// and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnHead(this TContainer @this, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, UrlPath.Root, HttpVerbs.Head, handler); + + /// + /// Creates an instance of that intercepts all OPTIONSrequests + /// under the specified and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnOptions(this TContainer @this, string baseRoute, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, baseRoute, HttpVerbs.Options, handler); + + /// + /// Creates an instance of that intercepts all OPTIONSrequests + /// and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnOptions(this TContainer @this, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, UrlPath.Root, HttpVerbs.Options, handler); + + /// + /// Creates an instance of that intercepts all PATCHrequests + /// under the specified and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnPatch(this TContainer @this, string baseRoute, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, baseRoute, HttpVerbs.Patch, handler); + + /// + /// Creates an instance of that intercepts all PATCHrequests + /// and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnPatch(this TContainer @this, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, UrlPath.Root, HttpVerbs.Patch, handler); + + /// + /// Creates an instance of that intercepts all POSTrequests + /// under the specified and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnPost(this TContainer @this, string baseRoute, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, baseRoute, HttpVerbs.Post, handler); + + /// + /// Creates an instance of that intercepts all POSTrequests + /// and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnPost(this TContainer @this, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, UrlPath.Root, HttpVerbs.Post, handler); + + /// + /// Creates an instance of that intercepts all PUTrequests + /// under the specified and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnPut(this TContainer @this, string baseRoute, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, baseRoute, HttpVerbs.Put, handler); + + /// + /// Creates an instance of that intercepts all PUTrequests + /// and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The callback used to handle requests. + /// with a added. + /// is . + /// + /// + /// + public static TContainer OnPut(this TContainer @this, RequestHandlerCallback handler) + where TContainer : class, IWebModuleContainer + => WithAction(@this, UrlPath.Root, HttpVerbs.Put, handler); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-Cors.cs b/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-Cors.cs new file mode 100644 index 0000000..f684892 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-Cors.cs @@ -0,0 +1,53 @@ +using System; +using EmbedIO.Cors; +using EmbedIO.Utilities; +using Swan; + +namespace EmbedIO +{ + partial class WebModuleContainerExtensions + { + /// + /// Creates an instance of and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// The valid origins. Default is "*", meaning all origins. + /// The valid headers. Default is "*", meaning all headers. + /// The valid method. Default is "*", meaning all methods. + /// with a added. + /// is . + /// + public static TContainer WithCors( + this TContainer @this, + string baseRoute, + string origins, + string headers, + string methods) + where TContainer : class, IWebModuleContainer + { + @this.Modules.Add(new CorsModule(baseRoute, origins, headers, methods)); + return @this; + } + + /// + /// Creates an instance of and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The valid origins. Default is "*", meaning all origins. + /// The valid headers. Default is "*", meaning all headers. + /// The valid method. Default is "*", meaning all methods. + /// with a added. + /// is . + /// + public static TContainer WithCors( + this TContainer @this, + string origins = CorsModule.All, + string headers = CorsModule.All, + string methods = CorsModule.All) + where TContainer : class, IWebModuleContainer + => WithCors(@this, UrlPath.Root, origins, headers, methods); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-Files.cs b/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-Files.cs new file mode 100644 index 0000000..ce506ad --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-Files.cs @@ -0,0 +1,275 @@ +using System; +using System.IO; +using System.Reflection; +using EmbedIO.Files; +using EmbedIO.Utilities; + +namespace EmbedIO +{ + partial class WebModuleContainerExtensions + { + /// + /// Creates an instance of , uses it to initialize + /// a , and adds the latter to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// The path of the directory to serve. + /// if files and directories in + /// are not expected to change during a web server's + /// lifetime; otherwise. + /// A callback used to configure the module. + /// with a added. + /// is . + /// is . + /// is not a valid local path. + /// + /// + /// + /// + public static TContainer WithStaticFolder( + this TContainer @this, + string baseRoute, + string fileSystemPath, + bool isImmutable, + Action? configure = null) + where TContainer : class, IWebModuleContainer + => WithStaticFolder(@this, null, baseRoute, fileSystemPath, isImmutable, configure); + + /// + /// Creates an instance of , uses it to initialize + /// a , and adds the latter to a module container, + /// giving it the specified if not . + /// + /// + /// OSX doesn't support , the parameter will be always . + /// + /// The type of the module container. + /// The on which this method is called. + /// The name. + /// The base route of the module. + /// The path of the directory to serve. + /// if files and directories in + /// are not expected to change during a web server's + /// lifetime; otherwise. + /// A callback used to configure the module. + /// with a added. + /// is . + /// is . + /// is not a valid local path. + /// + /// + /// + /// + public static TContainer WithStaticFolder( + this TContainer @this, + string? name, + string baseRoute, + string fileSystemPath, + bool isImmutable, + Action? configure = null) + where TContainer : class, IWebModuleContainer + { +#pragma warning disable CA2000 // Call Dispose on disposable - Ownership of provider is transferred to module + var provider = new FileSystemProvider(fileSystemPath, isImmutable); +#pragma warning restore CA2000 + try + { + var module = new FileModule(baseRoute, provider); + return WithModule(@this, name, module, configure); + } + catch + { + provider.Dispose(); + throw; + } + } + + /// + /// Creates an instance of , uses it to initialize + /// a , and adds the latter to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// The assembly where served files are contained as embedded resources. + /// A string to prepend to provider-specific paths + /// to form the name of a manifest resource in . + /// A callback used to configure the module. + /// with a added. + /// is . + /// is . + /// + /// + /// + /// + public static TContainer WithEmbeddedResources( + this TContainer @this, + string baseRoute, + Assembly assembly, + string pathPrefix, + Action? configure = null) + where TContainer : class, IWebModuleContainer + => WithEmbeddedResources(@this, null, baseRoute, assembly, pathPrefix, configure); + + /// + /// Creates an instance of , uses it to initialize + /// a , and adds the latter to a module container, + /// giving it the specified if not . + /// + /// The type of the module container. + /// The on which this method is called. + /// The name. + /// The base route of the module. + /// The assembly where served files are contained as embedded resources. + /// A string to prepend to provider-specific paths + /// to form the name of a manifest resource in . + /// A callback used to configure the module. + /// with a added. + /// is . + /// is . + /// + /// + /// + /// + public static TContainer WithEmbeddedResources( + this TContainer @this, + string? name, + string baseRoute, + Assembly assembly, + string pathPrefix, + Action? configure = null) + where TContainer : class, IWebModuleContainer + { + var module = new FileModule(baseRoute, new ResourceFileProvider(assembly, pathPrefix)); + return WithModule(@this, name, module, configure); + } + + /// + /// Creates an instance of using a file-system path, uses it to initialize + /// a , and adds the latter to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// The local path of the Zip file. + /// A callback used to configure the module. + /// with a added. + /// is . + /// + /// + /// + /// + public static TContainer WithZipFile( + this TContainer @this, + string baseRoute, + string zipFilePath, + Action? configure = null) + where TContainer : class, IWebModuleContainer + => WithZipFile(@this, null, baseRoute, zipFilePath, configure); + + /// + /// Creates an instance of using a file-system path, uses it to initialize + /// a , and adds the latter to a module container, + /// giving it the specified if not . + /// + /// The type of the module container. + /// The on which this method is called. + /// The name. + /// The base route of the module. + /// The zip file-system path. + /// A callback used to configure the module. + /// with a added. + /// is . + /// + /// + /// + /// + public static TContainer WithZipFile( + this TContainer @this, + string? name, + string baseRoute, + string zipFilePath, + Action? configure = null) + where TContainer : class, IWebModuleContainer + { +#pragma warning disable CA2000 // Call Dispose on disposable - Ownership of provider is transferred to module + var provider = new ZipFileProvider(zipFilePath); +#pragma warning restore CA2000 + try + { + var module = new FileModule(baseRoute, provider); + return WithModule(@this, name, module, configure); + } + catch + { + provider.Dispose(); + throw; + } + } + + /// + /// Creates an instance of using a zip file as stream, uses it to initialize + /// a , and adds the latter to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// The zip file as stream. + /// A callback used to configure the module. + /// with a added. + /// is . + /// + /// + /// + /// + public static TContainer WithZipFileStream( + this TContainer @this, + string baseRoute, + Stream zipFileStream, + Action? configure = null) + where TContainer : class, IWebModuleContainer + => WithZipFileStream(@this, null, baseRoute, zipFileStream, configure); + + /// + /// Creates an instance of using a zip file as stream, uses it to initialize + /// a , and adds the latter to a module container, + /// giving it the specified if not . + /// + /// The type of the module container. + /// The on which this method is called. + /// The name. + /// The base route of the module. + /// The zip file as stream. + /// A callback used to configure the module. + /// with a added. + /// is . + /// + /// + /// + /// + public static TContainer WithZipFileStream( + this TContainer @this, + string? name, + string baseRoute, + Stream zipFileStream, + Action? configure = null) + where TContainer : class, IWebModuleContainer + { +#pragma warning disable CA2000 // Call Dispose on disposable - Ownership of provider is transferred to module + var provider = new ZipFileProvider(zipFileStream); +#pragma warning restore CA2000 + try + { + var module = new FileModule(baseRoute, provider); + return WithModule(@this, name, module, configure); + } + catch + { + provider.Dispose(); + throw; + } + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-Routing.cs b/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-Routing.cs new file mode 100644 index 0000000..0be9d59 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-Routing.cs @@ -0,0 +1,51 @@ +using EmbedIO.Routing; +using EmbedIO.Utilities; +using System; + +namespace EmbedIO +{ + partial class WebModuleContainerExtensions + { + /// + /// Creates an instance of and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// A callback used to configure the newly-created . + /// with a added. + /// is . + /// is . + /// + /// + /// + /// + public static TContainer WithRouting(this TContainer @this, string baseRoute, Action configure) + where TContainer : class, IWebModuleContainer + => WithRouting(@this, null, baseRoute, configure); + + /// + /// Creates an instance of and adds it to a module container, + /// giving it the specified if not . + /// + /// The type of the module container. + /// The on which this method is called. + /// The name. + /// The base route of the module. + /// A callback used to configure the newly-created . + /// with a added. + /// is . + /// is . + /// + /// + /// + /// + public static TContainer WithRouting(this TContainer @this, string? name, string baseRoute, Action configure) + where TContainer : class, IWebModuleContainer + { + configure = Validate.NotNull(nameof(configure), configure); + var module = new RoutingModule(baseRoute); + return WithModule(@this, name, module, configure); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-Security.cs b/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-Security.cs new file mode 100644 index 0000000..03cde8c --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-Security.cs @@ -0,0 +1,43 @@ +using EmbedIO.Security; +using System; +using System.Collections.Generic; + +namespace EmbedIO +{ + partial class WebModuleContainerExtensions + { + /// + /// Creates an instance of and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// A collection of valid IPs that never will be banned. + /// Minutes that an IP will remain banned. + /// + /// with an added. + /// + public static TContainer WithIPBanning(this TContainer @this, + IEnumerable? whiteList = null, + int banMinutes = IPBanningModule.DefaultBanMinutes) + where TContainer : class, IWebModuleContainer => + WithIPBanning(@this, null, whiteList, banMinutes); + + /// + /// Creates an instance of and adds it to a module container. + /// + /// The type of the module container. + /// The on which this method is called. + /// The configure. + /// A collection of valid IPs that never will be banned. + /// Minutes that an IP will remain banned. + /// + /// with an added. + /// + public static TContainer WithIPBanning(this TContainer @this, + Action? configure, + IEnumerable? whiteList = null, + int banMinutes = IPBanningModule.DefaultBanMinutes) + where TContainer : class, IWebModuleContainer => + WithModule(@this, new IPBanningModule("/", whiteList, banMinutes), configure); + } +} diff --git a/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-WebApi.cs b/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-WebApi.cs new file mode 100644 index 0000000..a56a44d --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions-WebApi.cs @@ -0,0 +1,126 @@ +using System; +using System.Threading.Tasks; +using EmbedIO.Routing; +using EmbedIO.Utilities; +using EmbedIO.WebApi; + +namespace EmbedIO +{ + partial class WebModuleContainerExtensions + { + /// + /// Creates an instance of using the default response serializer + /// and adds it to a module container without giving it a name. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// A callback used to configure the newly-created . + /// with a added. + /// is . + /// is . + /// + /// + /// + /// + public static TContainer WithWebApi(this TContainer @this, string baseRoute, Action configure) + where TContainer : class, IWebModuleContainer + => WithWebApi(@this, null, baseRoute, configure); + + /// + /// Creates an instance of using the specified response serializer + /// and adds it to a module container without giving it a name. + /// + /// The type of the module container. + /// The on which this method is called. + /// The base route of the module. + /// A used to serialize + /// the result of controller methods returning + /// or Task<object>. + /// A callback used to configure the newly-created . + /// with a added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// + /// + /// + /// + public static TContainer WithWebApi( + this TContainer @this, + string baseRoute, + ResponseSerializerCallback serializer, + Action configure) + where TContainer : class, IWebModuleContainer + => WithWebApi(@this, null, baseRoute, serializer, configure); + + /// + /// Creates an instance of using the default response serializer + /// and adds it to a module container, giving it the specified + /// if not + /// + /// The type of the module container. + /// The on which this method is called. + /// The name. + /// The base route of the module. + /// A callback used to configure the newly-created . + /// with a added. + /// is . + /// is . + /// + /// + /// + /// + public static TContainer WithWebApi( + this TContainer @this, + string? name, + string baseRoute, + Action configure) + where TContainer : class, IWebModuleContainer + { + configure = Validate.NotNull(nameof(configure), configure); + var module = new WebApiModule(baseRoute); + return WithModule(@this, name, module, configure); + } + + /// + /// Creates an instance of , using the specified response serializer + /// and adds it to a module container, giving it the specified + /// if not + /// + /// The type of the module container. + /// The on which this method is called. + /// The name. + /// The base route of the module. + /// A used to serialize + /// the result of controller methods returning + /// or Task<object>. + /// A callback used to configure the newly-created . + /// with a added. + /// is . + /// + /// is . + /// - or - + /// is . + /// + /// + /// + /// + /// + public static TContainer WithWebApi( + this TContainer @this, + string? name, + string baseRoute, + ResponseSerializerCallback serializer, + Action configure) + where TContainer : class, IWebModuleContainer + { + configure = Validate.NotNull(nameof(configure), configure); + var module = new WebApiModule(baseRoute, serializer); + return WithModule(@this, name, module, configure); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions.cs b/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions.cs new file mode 100644 index 0000000..57323e7 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebModuleContainerExtensions.cs @@ -0,0 +1,84 @@ +using System; +using EmbedIO.Utilities; + +namespace EmbedIO +{ + /// + /// Contains extension methods for types implementing . + /// + public static partial class WebModuleContainerExtensions + { + /// + /// Adds the specified to a module container, without giving it a name. + /// + /// The type of the module container. + /// The on which this method is called. + /// The module. + /// with added. + /// is . + /// + /// + public static TContainer WithModule(this TContainer @this, IWebModule module) + where TContainer : class, IWebModuleContainer + => WithModule(@this, null, module); + + /// + /// Adds the specified to a module container, + /// giving it the specified if not . + /// + /// The type of the module container. + /// The on which this method is called. + /// The name. + /// The module. + /// with added. + /// is . + /// + /// + public static TContainer WithModule(this TContainer @this, string? name, IWebModule module) + where TContainer : class, IWebModuleContainer + { + @this.Modules.Add(name, module); + return @this; + } + + /// + /// Adds the specified to a module container, without giving it a name. + /// + /// The type of the module container. + /// The type of the . + /// The on which this method is called. + /// The module. + /// A callback used to configure the . + /// with added. + /// is . + /// + /// + public static TContainer WithModule(this TContainer @this, TWebModule module, Action? configure) + where TContainer : class, IWebModuleContainer + where TWebModule : IWebModule + => WithModule(@this, null, module, configure); + + /// + /// Adds the specified to a module container, + /// giving it the specified if not . + /// + /// The type of the module container. + /// The type of the . + /// The on which this method is called. + /// The name. + /// The module. + /// A callback used to configure the . + /// with added. + /// is . + /// + /// + public static TContainer WithModule(this TContainer @this, string? name, TWebModule module, Action? configure) + where TContainer : class, IWebModuleContainer + where TWebModule : IWebModule + { + configure?.Invoke(module); + @this.Modules.Add(name, module); + return @this; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebModuleExtensions-ExceptionHandlers.cs b/Vendor/EmbedIO-3.5.2/WebModuleExtensions-ExceptionHandlers.cs new file mode 100644 index 0000000..2d4f49d --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebModuleExtensions-ExceptionHandlers.cs @@ -0,0 +1,45 @@ +using System; + +namespace EmbedIO +{ + partial class WebModuleExtensions + { + /// + /// Sets the HTTP exception handler on an . + /// + /// The type of the web server. + /// The on which this method is called. + /// The HTTP exception handler. + /// with the OnHttpException + /// property set to . + /// is . + /// The module's configuration is locked. + /// + /// + public static TWebModule HandleHttpException(this TWebModule @this, HttpExceptionHandlerCallback handler) + where TWebModule : IWebModule + { + @this.OnHttpException = handler; + return @this; + } + + /// + /// Sets the unhandled exception handler on an . + /// + /// The type of the web server. + /// The on which this method is called. + /// The unhandled exception handler. + /// with the OnUnhandledException + /// property set to . + /// is . + /// The module's configuration is locked. + /// + /// + public static TWebModule HandleUnhandledException(this TWebModule @this, ExceptionHandlerCallback handler) + where TWebModule : IWebModule + { + @this.OnUnhandledException = handler; + return @this; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebModuleExtensions.cs b/Vendor/EmbedIO-3.5.2/WebModuleExtensions.cs new file mode 100644 index 0000000..b691abb --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebModuleExtensions.cs @@ -0,0 +1,9 @@ +namespace EmbedIO +{ + /// + /// Provides extension methods for types implementing . + /// + public static partial class WebModuleExtensions + { + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebServer-Constants.cs b/Vendor/EmbedIO-3.5.2/WebServer-Constants.cs new file mode 100644 index 0000000..78e47d3 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebServer-Constants.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace EmbedIO +{ + partial class WebServer + { + /// + /// The size, in bytes,of buffers used to transfer contents between streams. + /// The value of this constant is the same as the default used by the + /// method. For the reasons why this value was chosen, see + /// .NET Framework reference source. + /// + public const int StreamCopyBufferSize = 81920; + + /// + /// The scheme of . + /// + public const string UriSchemeNull = "null"; + + /// + /// The signature string included in Server response headers. + /// + public static readonly string Signature = "com.thequux.Stationeers.RemoteControl"; + + /// + /// An that can be used to send UTF-8 responses without a byte order mark (BOM). + /// This is the default encoding used by and should be used instead of + /// when specifying an encoding for . + /// + public static readonly Encoding Utf8NoBomEncoding = new UTF8Encoding(false); + + /// + /// The default encoding that is both assumed for requests that do not specify an encoding, + /// and used for responses when an encoding is not specified. + /// This is the same as . + /// + public static readonly Encoding DefaultEncoding = Utf8NoBomEncoding; + + /// + /// An which cannot be equal to any HTTP / HTTP URI. + /// Used as the default value for non-nullable properties of type . + /// + public static readonly Uri NullUri = new ("null:"); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebServer.cs b/Vendor/EmbedIO-3.5.2/WebServer.cs new file mode 100644 index 0000000..f68382d --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebServer.cs @@ -0,0 +1,191 @@ +using System; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Net.Internal; +using EmbedIO.Routing; +using EmbedIO.Utilities; +using Swan.Logging; + +namespace EmbedIO +{ + /// + /// EmbedIO's web server. This is the default implementation of . + /// This class also contains some useful constants related to EmbedIO's internal working. + /// + public partial class WebServer : WebServerBase + { + /// + /// Initializes a new instance of the class, + /// that will respond on HTTP port 80 on all network interfaces. + /// + public WebServer() + : this(80) + { + } + + /// + /// Initializes a new instance of the class, + /// that will respond on the specified HTTP port on all network interfaces. + /// + /// The port. + public WebServer(int port) + : this($"http://*:{port}/") + { + } + + /// + /// Initializes a new instance of the class + /// with the specified URL prefixes. + /// + /// The URL prefixes to configure. + /// is . + /// + /// One or more of the elements of is the empty string. + /// - or - + /// One or more of the elements of is already registered. + /// + public WebServer(params string[] urlPrefixes) + : this(new WebServerOptions().WithUrlPrefixes(urlPrefixes)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The type of HTTP listener to configure. + /// The URL prefixes to configure. + /// is . + /// + /// One or more of the elements of is the empty string. + /// - or - + /// One or more of the elements of is already registered. + /// + public WebServer(HttpListenerMode mode, params string[] urlPrefixes) + : this(new WebServerOptions().WithMode(mode).WithUrlPrefixes(urlPrefixes)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The type of HTTP listener to configure. + /// The X.509 certificate to use for SSL connections. + /// The URL prefixes to configure. + /// is . + /// + /// One or more of the elements of is the empty string. + /// - or - + /// One or more of the elements of is already registered. + /// + public WebServer(HttpListenerMode mode, X509Certificate2 certificate, params string[] urlPrefixes) + : this(new WebServerOptions() + .WithMode(mode) + .WithCertificate(certificate) + .WithUrlPrefixes(urlPrefixes)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// A object used to configure this instance. + /// is . + public WebServer(WebServerOptions options) + : base(options) + { + Listener = CreateHttpListener(); + } + + /// + /// Initializes a new instance of the class. + /// + /// A callback that will be used to configure + /// the server's options. + /// is . + public WebServer(Action configure) + : base(configure) + { + Listener = CreateHttpListener(); + } + + /// + /// Gets the underlying HTTP listener. + /// + public IHttpListener Listener { get; } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + try + { + Listener.Dispose(); + } + catch (Exception ex) + { + ex.Log(LogSource, "Exception thrown while disposing HTTP listener."); + } + + "Listener closed.".Info(LogSource); + } + + base.Dispose(disposing); + } + + /// + protected override void Prepare(CancellationToken cancellationToken) + { + Listener.Start(); + "Started HTTP Listener".Info(LogSource); + + // close port when the cancellation token is cancelled + _ = cancellationToken.Register(() => Listener?.Stop()); + } + + /// + protected override async Task ProcessRequestsAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested && (Listener?.IsListening ?? false)) + { + var context = await Listener.GetContextAsync(cancellationToken).ConfigureAwait(false); + context.CancellationToken = cancellationToken; + context.Route = RouteMatch.UnsafeFromRoot(UrlPath.Normalize(context.Request.Url.AbsolutePath, false)); + +#pragma warning disable CS4014 // Call is not awaited - of course, it has to run in parallel. + _ = Task.Run(() => DoHandleContextAsync(context), cancellationToken); +#pragma warning restore CS4014 + } + } + + /// + protected override void OnFatalException() => Listener?.Dispose(); + + private IHttpListener CreateHttpListener() + { + IHttpListener DoCreate() => Options.Mode switch { + HttpListenerMode.Microsoft => System.Net.HttpListener.IsSupported + ? new SystemHttpListener(new System.Net.HttpListener()) as IHttpListener + : new Net.HttpListener(Options.Certificate), + _ => new Net.HttpListener(Options.Certificate) + }; + + var listener = DoCreate(); + $"Running HTTPListener: {listener.Name}".Info(LogSource); + + foreach (var prefix in Options.UrlPrefixes) + { + var urlPrefix = new string(prefix?.ToCharArray()); + + if (!urlPrefix.EndsWith("/")) urlPrefix += "/"; + urlPrefix = urlPrefix.ToLowerInvariant(); + + listener.AddPrefix(urlPrefix); + $"Web server prefix '{urlPrefix}' added.".Info(LogSource); + } + + return listener; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebServerBase`1.cs b/Vendor/EmbedIO-3.5.2/WebServerBase`1.cs new file mode 100644 index 0000000..6230bc6 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebServerBase`1.cs @@ -0,0 +1,349 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Internal; +using EmbedIO.Sessions; +using EmbedIO.Utilities; +using Swan.Configuration; +using Swan.Logging; + +namespace EmbedIO +{ + /// + /// Base class for implementations. + /// + /// The type of the options object used to configure an instance. + /// + /// + public abstract class WebServerBase : ConfiguredObject, IWebServer, IHttpContextHandler + where TOptions : WebServerOptionsBase, new() + { + private readonly WebModuleCollection _modules; + + private readonly MimeTypeCustomizer _mimeTypeCustomizer = new MimeTypeCustomizer(); + + private ExceptionHandlerCallback _onUnhandledException = ExceptionHandler.Default; + private HttpExceptionHandlerCallback _onHttpException = HttpExceptionHandler.Default; + + private WebServerState _state = WebServerState.Created; + + private ISessionManager? _sessionManager; + + /// + /// Initializes a new instance of the class. + /// + protected WebServerBase() + : this(new TOptions(), null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// A instance that will be used + /// to configure the server. + /// is . + protected WebServerBase(TOptions options) + : this(Validate.NotNull(nameof(options), options), null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// A callback that will be used to configure + /// the server's options. + /// is . + protected WebServerBase(Action configure) + : this(new TOptions(), Validate.NotNull(nameof(configure), configure)) + { + } + + private WebServerBase(TOptions options, Action? configure) + { + Options = options; + LogSource = GetType().Name; + _modules = new WebModuleCollection(LogSource); + + configure?.Invoke(Options); + Options.Lock(); + } + + /// + /// Finalizes an instance of the class. + /// + ~WebServerBase() + { + Dispose(false); + } + + /// + public event WebServerStateChangedEventHandler? StateChanged; + + /// + public IComponentCollection Modules => _modules; + + /// + /// Gets the options object used to configure this instance. + /// + public TOptions Options { get; } + + /// + /// The server's configuration is locked. + /// this property is being set to . + /// + /// The default value for this property is . + /// + /// + public ExceptionHandlerCallback OnUnhandledException + { + get => _onUnhandledException; + set + { + EnsureConfigurationNotLocked(); + _onUnhandledException = Validate.NotNull(nameof(value), value); + } + } + + /// + /// The server's configuration is locked. + /// this property is being set to . + /// + /// The default value for this property is . + /// + /// + public HttpExceptionHandlerCallback OnHttpException + { + get => _onHttpException; + set + { + EnsureConfigurationNotLocked(); + _onHttpException = Validate.NotNull(nameof(value), value); + } + } + + /// + public ISessionManager? SessionManager + { + get => _sessionManager; + set + { + EnsureConfigurationNotLocked(); + _sessionManager = value; + } + } + + /// + public WebServerState State + { + get => _state; + private set + { + if (value == _state) return; + + var oldState = _state; + _state = value; + + if (_state != WebServerState.Created) + { + LockConfiguration(); + } + + StateChanged?.Invoke(this, new WebServerStateChangedEventArgs(oldState, value)); + } + } + + /// + /// Gets a string to use as a source for log messages. + /// + protected string LogSource { get; } + + /// + public Task HandleContextAsync(IHttpContextImpl context) + { + if (State > WebServerState.Listening) + throw new InvalidOperationException("The web server has already been stopped."); + + if (State < WebServerState.Listening) + throw new InvalidOperationException("The web server has not been started yet."); + + return DoHandleContextAsync(context); + } + + string IMimeTypeProvider.GetMimeType(string extension) + => _mimeTypeCustomizer.GetMimeType(extension); + + bool IMimeTypeProvider.TryDetermineCompression(string mimeType, out bool preferCompression) + => _mimeTypeCustomizer.TryDetermineCompression(mimeType, out preferCompression); + + /// + public void AddCustomMimeType(string extension, string mimeType) + => _mimeTypeCustomizer.AddCustomMimeType(extension, mimeType); + + /// + public void PreferCompression(string mimeType, bool preferCompression) + => _mimeTypeCustomizer.PreferCompression(mimeType, preferCompression); + + /// + /// The method was already called. + /// Cancellation was requested. + public async Task RunAsync(CancellationToken cancellationToken = default) + { + try + { + State = WebServerState.Loading; + Prepare(cancellationToken); + + _sessionManager?.Start(cancellationToken); + _modules.StartAll(cancellationToken); + + State = WebServerState.Listening; + await ProcessRequestsAsync(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + "Operation canceled.".Debug(LogSource); + } + finally + { + "Cleaning up".Info(LogSource); + State = WebServerState.Stopped; + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Asynchronously handles a received request. + /// + /// The context of the request. + /// A representing the ongoing operation. + protected async Task DoHandleContextAsync(IHttpContextImpl context) + { + context.SupportCompressedRequests = Options.SupportCompressedRequests; + context.MimeTypeProviders.Push(this); + + try + { + $"[{context.Id}] {context.Request.SafeGetRemoteEndpointStr()}: {context.Request.HttpMethod} {context.Request.Url.PathAndQuery} - {context.Request.UserAgent}" + .Debug(LogSource); + + if (SessionManager != null) + context.Session = new SessionProxy(context, SessionManager); + + try + { + if (context.CancellationToken.IsCancellationRequested) + return; + + try + { + // Return a 404 (Not Found) response if no module handled the response. + await _modules.DispatchRequestAsync(context).ConfigureAwait(false); + if (!context.IsHandled) + { + $"[{context.Id}] No module generated a response. Sending 404 - Not Found".Error(LogSource); + throw HttpException.NotFound("No module was able to serve the requested path."); + } + } + catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) + { + throw; // Let outer catch block handle it + } + catch (HttpListenerException) + { + throw; // Let outer catch block handle it + } + catch (Exception exception) when (exception is IHttpException) + { + await HttpExceptionHandler.Handle(LogSource, context, exception, _onHttpException) + .ConfigureAwait(false); + } + catch (Exception exception) + { + await ExceptionHandler.Handle(LogSource, context, exception, _onUnhandledException, _onHttpException) + .ConfigureAwait(false); + } + } + finally + { + await context.Response.OutputStream.FlushAsync(context.CancellationToken) + .ConfigureAwait(false); + + var statusCode = context.Response.StatusCode; + var statusDescription = context.Response.StatusDescription; + var sendChunked = context.Response.SendChunked; + var contentLength = context.Response.ContentLength64; + context.Close(); + $"[{context.Id}] {context.Request.HttpMethod} {context.Request.Url.AbsolutePath}: \"{statusCode} {statusDescription}\" sent in {context.Age}ms ({(sendChunked ? "chunked" : contentLength.ToString(CultureInfo.InvariantCulture) + " bytes")})" + .Info(LogSource); + } + } + catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) + { + $"[{context.Id}] Operation canceled.".Debug(LogSource); + } + catch (HttpListenerException ex) + { + ex.Log(LogSource, $"[{context.Id}] Listener exception."); + } + catch (Exception ex) + { + ex.Log(LogSource, $"[{context.Id}] Fatal exception."); + OnFatalException(); + } + } + + /// + protected override void OnBeforeLockConfiguration() + { + base.OnBeforeLockConfiguration(); + + _mimeTypeCustomizer.Lock(); + _modules.Lock(); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (!disposing) + return; + + _modules.Dispose(); + } + + /// + /// Prepares a web server for running. + /// + /// A used to stop the web server. + protected virtual void Prepare(CancellationToken cancellationToken) + { + } + + /// + /// Asynchronously receives requests and processes them. + /// + /// A used to stop the web server. + /// A representing the ongoing operation. + protected abstract Task ProcessRequestsAsync(CancellationToken cancellationToken); + + /// + /// Called when an exception is caught in the web server's request processing loop. + /// This method should tell the server socket to stop accepting further requests. + /// + protected abstract void OnFatalException(); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebServerExtensions-ExceptionHandliers.cs b/Vendor/EmbedIO-3.5.2/WebServerExtensions-ExceptionHandliers.cs new file mode 100644 index 0000000..fd716b8 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebServerExtensions-ExceptionHandliers.cs @@ -0,0 +1,47 @@ +using System; + +namespace EmbedIO +{ + partial class WebServerExtensions + { + /// + /// Sets the HTTP exception handler on an . + /// + /// The type of the web server. + /// The on which this method is called. + /// The HTTP exception handler. + /// with the OnHttpException + /// property set to . + /// is . + /// The web server has already been started. + /// is . + /// + /// + public static TWebServer HandleHttpException(this TWebServer @this, HttpExceptionHandlerCallback handler) + where TWebServer : IWebServer + { + @this.OnHttpException = handler; + return @this; + } + + /// + /// Sets the unhandled exception handler on an . + /// + /// The type of the web server. + /// The on which this method is called. + /// The unhandled exception handler. + /// with the OnUnhandledException + /// property set to . + /// is . + /// The web server has already been started. + /// is . + /// + /// + public static TWebServer HandleUnhandledException(this TWebServer @this, ExceptionHandlerCallback handler) + where TWebServer : IWebServer + { + @this.OnUnhandledException = handler; + return @this; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebServerExtensions-SessionManager.cs b/Vendor/EmbedIO-3.5.2/WebServerExtensions-SessionManager.cs new file mode 100644 index 0000000..65844a8 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebServerExtensions-SessionManager.cs @@ -0,0 +1,43 @@ +using System; +using EmbedIO.Sessions; + +namespace EmbedIO +{ + partial class WebServerExtensions + { + /// + /// Sets the session manager on an . + /// + /// The type of the web server. + /// The on which this method is called. + /// The session manager. + /// with the session manager set. + /// is . + /// The web server has already been started. + public static TWebServer WithSessionManager(this TWebServer @this, ISessionManager sessionManager) + where TWebServer : IWebServer + { + @this.SessionManager = sessionManager; + return @this; + } + + /// + /// Creates a with all properties set to their default values + /// and sets it as session manager on an . + /// + /// The type of the web server. + /// The on which this method is called. + /// A callback used to configure the session manager. + /// with the session manager set. + /// is . + /// The web server has already been started. + public static TWebServer WithLocalSessionManager(this TWebServer @this, Action? configure = null) + where TWebServer : IWebServer + { + var sessionManager = new LocalSessionManager(); + configure?.Invoke(sessionManager); + @this.SessionManager = sessionManager; + return @this; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebServerExtensions.cs b/Vendor/EmbedIO-3.5.2/WebServerExtensions.cs new file mode 100644 index 0000000..43aaec2 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebServerExtensions.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Swan; + +namespace EmbedIO +{ + /// + /// Provides extension methods for types implementing . + /// + public static partial class WebServerExtensions + { + /// + /// Starts a web server by calling + /// in another thread. + /// + /// The on which this method is called. + /// A used to stop the web server. + /// is . + /// The web server has already been started. + public static void Start(this IWebServer @this, CancellationToken cancellationToken = default) + { +#pragma warning disable CS4014 // The call is not awaited - it is expected to run in parallel. + Task.Run(() => @this.RunAsync(cancellationToken)); +#pragma warning restore CS4014 + while (@this.State < WebServerState.Listening) + Task.Delay(1, cancellationToken).Await(); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebServerOptions.cs b/Vendor/EmbedIO-3.5.2/WebServerOptions.cs new file mode 100644 index 0000000..2700a9f --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebServerOptions.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.RegularExpressions; +using EmbedIO.Utilities; +using Swan; +using Swan.Logging; + +namespace EmbedIO +{ + /// + /// Contains options for configuring an instance of . + /// + public sealed class WebServerOptions : WebServerOptionsBase + { + private const string NetShLogSource = "NetSh"; + + private readonly List _urlPrefixes = new List(); + + private HttpListenerMode _mode = HttpListenerMode.EmbedIO; + + private X509Certificate2? _certificate; + + private string? _certificateThumbprint; + + private bool _autoLoadCertificate; + + private bool _autoRegisterCertificate; + + private StoreName _storeName = StoreName.My; + + private StoreLocation _storeLocation = StoreLocation.LocalMachine; + + /// + /// Gets the URL prefixes. + /// + public IReadOnlyList UrlPrefixes => _urlPrefixes; + + /// + /// Gets or sets the type of HTTP listener. + /// + /// This property is being set, + /// and this instance's configuration is locked. + /// + public HttpListenerMode Mode + { + get => _mode; + set + { + EnsureConfigurationNotLocked(); + _mode = value; + } + } + + /// + /// Gets or sets the X.509 certificate to use for SSL connections. + /// + /// This property is being set, + /// and this instance's configuration is locked. + public X509Certificate2? Certificate + { + get + { + if (AutoRegisterCertificate) + return TryRegisterCertificate() ? _certificate : null; + + return _certificate ?? (AutoLoadCertificate ? LoadCertificate() : null); + } + set + { + EnsureConfigurationNotLocked(); + _certificate = value; + } + } + + /// + /// Gets or sets the thumbprint of the X.509 certificate to use for SSL connections. + /// + /// This property is being set, + /// and this instance's configuration is locked. + public string? CertificateThumbprint + { + get => _certificateThumbprint; + set + { + EnsureConfigurationNotLocked(); + + // strip any non-hexadecimal values and make uppercase + _certificateThumbprint = value == null + ? null + : Regex.Replace(value, @"[^\da-fA-F]", string.Empty).ToUpper(CultureInfo.InvariantCulture); + } + } + + /// + /// Gets or sets a value indicating whether to automatically load the X.509 certificate. + /// + /// This property is being set, + /// and this instance's configuration is locked. + /// This property is being set to + /// and the underlying operating system is not Windows. + public bool AutoLoadCertificate + { + get => _autoLoadCertificate; + set + { + EnsureConfigurationNotLocked(); + if (value && SwanRuntime.OS != Swan.OperatingSystem.Windows) + throw new PlatformNotSupportedException("AutoLoadCertificate functionality is only available under Windows."); + + _autoLoadCertificate = value; + } + } + + /// + /// Gets or sets a value indicating whether to automatically bind the X.509 certificate + /// to the port used for HTTPS. + /// + /// This property is being set, + /// and this instance's configuration is locked. + /// This property is being set to + /// and the underlying operating system is not Windows. + public bool AutoRegisterCertificate + { + get => _autoRegisterCertificate; + set + { + EnsureConfigurationNotLocked(); + if (value && SwanRuntime.OS != Swan.OperatingSystem.Windows) + throw new PlatformNotSupportedException("AutoRegisterCertificate functionality is only available under Windows."); + + _autoRegisterCertificate = value; + } + } + + /// + /// Gets or sets a value indicating the X.509 certificate store where to load the certificate from. + /// + /// This property is being set, + /// and this instance's configuration is locked. + /// + public StoreName StoreName + { + get => _storeName; + set + { + EnsureConfigurationNotLocked(); + _storeName = value; + } + } + + /// + /// Gets or sets a value indicating the location of the X.509 certificate store where to load the certificate from. + /// + /// This property is being set, + /// and this instance's configuration is locked. + /// + public StoreLocation StoreLocation + { + get => _storeLocation; + set + { + EnsureConfigurationNotLocked(); + _storeLocation = value; + } + } + + /// + /// Adds a URL prefix. + /// + /// The URL prefix. + /// This instance's configuration is locked. + /// is . + /// + /// is the empty string. + /// - or - + /// is already registered. + /// + public void AddUrlPrefix(string urlPrefix) + { + EnsureConfigurationNotLocked(); + + urlPrefix = Validate.NotNullOrEmpty(nameof(urlPrefix), urlPrefix); + if (_urlPrefixes.Contains(urlPrefix)) + throw new ArgumentException("URL prefix is already registered.", nameof(urlPrefix)); + + _urlPrefixes.Add(urlPrefix); + } + + private X509Certificate2? LoadCertificate() + { + if (SwanRuntime.OS != Swan.OperatingSystem.Windows) + return null; + + if (!string.IsNullOrWhiteSpace(_certificateThumbprint)) return GetCertificate(_certificateThumbprint); + + using var netsh = GetNetsh("show"); + + string? thumbprint = null; + + netsh.ErrorDataReceived += (s, e) => + { + if (string.IsNullOrWhiteSpace(e.Data)) return; + + e.Data.Error(NetShLogSource); + }; + + netsh.OutputDataReceived += (s, e) => + { + if (string.IsNullOrWhiteSpace(e.Data)) return; + + e.Data.Debug(NetShLogSource); + + var line = e.Data.Trim(); + + if (line.StartsWith("Certificate Hash") && line.IndexOf(":", StringComparison.Ordinal) > -1) + thumbprint = line.Split(':')[1].Trim(); + }; + + if (!netsh.Start()) + return null; + + netsh.BeginOutputReadLine(); + netsh.BeginErrorReadLine(); + netsh.WaitForExit(); + + return netsh.ExitCode == 0 && !string.IsNullOrEmpty(thumbprint) + ? GetCertificate(thumbprint) + : null; + } + + private X509Certificate2? GetCertificate(string? thumbprint = null) + { + using var store = new X509Store(StoreName, StoreLocation); + store.Open(OpenFlags.ReadOnly); + var signingCert = store.Certificates.Find( + X509FindType.FindByThumbprint, + thumbprint ?? _certificateThumbprint, + false); + return signingCert.Count == 0 ? null : signingCert[0]; + } + + private bool AddCertificateToStore() + { + using var store = new X509Store(StoreName, StoreLocation); + try + { + store.Open(OpenFlags.ReadWrite); + store.Add(_certificate); + return true; + } + catch + { + return false; + } + } + + private bool TryRegisterCertificate() + { + if (SwanRuntime.OS != Swan.OperatingSystem.Windows) + return false; + + if (_certificate == null) + throw new InvalidOperationException("A certificate is required to AutoRegister"); + + if (GetCertificate(_certificate.Thumbprint) == null && !AddCertificateToStore()) + { + throw new InvalidOperationException( + "The provided certificate cannot be added to the default store, add it manually"); + } + + using var netsh = GetNetsh("add", $"certhash={_certificate.Thumbprint} appid={{adaa04bb-8b63-4073-a12f-d6f8c0b4383f}}"); + + var sb = new StringBuilder(); + + void PushLine(object sender, DataReceivedEventArgs e) + { + if (string.IsNullOrWhiteSpace(e.Data)) return; + + sb.AppendLine(e.Data); + e.Data.Error(NetShLogSource); + } + + netsh.OutputDataReceived += PushLine; + + netsh.ErrorDataReceived += PushLine; + + if (!netsh.Start()) return false; + + netsh.BeginOutputReadLine(); + netsh.BeginErrorReadLine(); + netsh.WaitForExit(); + + return netsh.ExitCode == 0 ? true : throw new InvalidOperationException($"NetSh error: {sb}"); + } + + private int GetSslPort() + { + var port = 443; + + foreach (var url in UrlPrefixes.Where(x => + x.StartsWith("https:", StringComparison.OrdinalIgnoreCase))) + { + var match = Regex.Match(url, @":(\d+)"); + + if (match.Success && int.TryParse(match.Groups[1].Value, out port)) + break; + } + + return port; + } + + private Process GetNetsh(string verb, string options = "") => new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "netsh", + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + Arguments = $"http {verb} sslcert ipport=0.0.0.0:{GetSslPort()} {options}", + }, + }; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebServerOptionsBase.cs b/Vendor/EmbedIO-3.5.2/WebServerOptionsBase.cs new file mode 100644 index 0000000..dd4790a --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebServerOptionsBase.cs @@ -0,0 +1,35 @@ +using System; +using Swan.Configuration; + +namespace EmbedIO +{ + /// + /// Base class for web server options. + /// + public abstract class WebServerOptionsBase : ConfiguredObject + { + private bool _supportCompressedRequests; + + /// + /// Gets or sets a value indicating whether compressed request bodies are supported. + /// The default value is , because of the security risk + /// posed by decompression bombs. + /// + /// This property is being set and this instance's + /// configuration is locked. + public bool SupportCompressedRequests + { + get => _supportCompressedRequests; + set + { + EnsureConfigurationNotLocked(); + _supportCompressedRequests = value; + } + } + + /// + /// Locks this instance, preventing further configuration. + /// + public void Lock() => LockConfiguration(); + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebServerOptionsBaseExtensions.cs b/Vendor/EmbedIO-3.5.2/WebServerOptionsBaseExtensions.cs new file mode 100644 index 0000000..a100306 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebServerOptionsBaseExtensions.cs @@ -0,0 +1,27 @@ +using System; + +namespace EmbedIO +{ + /// + /// Provides extension methods for classes derived from . + /// + public static class WebServerOptionsBaseExtensions + { + /// + /// Adds a URL prefix. + /// + /// The type of the object on which this method is called. + /// The object on which this method is called. + /// If , enable support for compressed request bodies. + /// with its SupportCompressedRequests + /// property set to . + /// is . + /// The configuration of is locked. + public static TOptions WithSupportCompressedRequests(this TOptions @this, bool value) + where TOptions : WebServerOptionsBase + { + @this.SupportCompressedRequests = value; + return @this; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebServerOptionsExtensions.cs b/Vendor/EmbedIO-3.5.2/WebServerOptionsExtensions.cs new file mode 100644 index 0000000..b68b28d --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebServerOptionsExtensions.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using EmbedIO.Utilities; + +namespace EmbedIO +{ + /// + /// Provides extension methods for . + /// + public static class WebServerOptionsExtensions + { + /// + /// Adds a URL prefix. + /// + /// The on which this method is called. + /// The URL prefix. + /// with added. + /// is . + /// The configuration of is locked. + /// is . + /// + /// is the empty string. + /// - or - + /// is already registered. + /// + public static WebServerOptions WithUrlPrefix(this WebServerOptions @this, string urlPrefix) + { + @this.AddUrlPrefix(urlPrefix); + return @this; + } + + /// + /// Adds zero or more URL prefixes. + /// + /// The on which this method is called. + /// An enumeration of URL prefixes to add. + /// with every non- element + /// of added. + /// is . + /// The configuration of is locked. + /// is . + /// + /// One or more of the elements of is the empty string. + /// - or - + /// One or more of the elements of is already registered. + /// + public static WebServerOptions WithUrlPrefixes(this WebServerOptions @this, IEnumerable urlPrefixes) + { + foreach (var urlPrefix in Validate.NotNull(nameof(urlPrefixes), urlPrefixes)) + @this.AddUrlPrefix(urlPrefix); + + return @this; + } + + /// + /// Adds zero or more URL prefixes. + /// + /// The on which this method is called. + /// An array of URL prefixes to add. + /// with every non- element + /// of added. + /// is . + /// The configuration of is locked. + /// is . + /// + /// One or more of the elements of is the empty string. + /// - or - + /// One or more of the elements of is already registered. + /// + public static WebServerOptions WithUrlPrefixes(this WebServerOptions @this, params string[] urlPrefixes) + => WithUrlPrefixes(@this, urlPrefixes as IEnumerable); + + /// + /// Sets the type of HTTP listener. + /// + /// The on which this method is called. + /// The type of HTTP listener. + /// with its Mode property + /// set to . + /// is . + /// The configuration of is locked. + public static WebServerOptions WithMode(this WebServerOptions @this, HttpListenerMode value) + { + @this.Mode = value; + return @this; + } + + /// + /// Sets the type of HTTP listener to . + /// + /// The on which this method is called. + /// with its Mode property + /// set to . + /// is . + /// The configuration of is locked. + public static WebServerOptions WithEmbedIOHttpListener(this WebServerOptions @this) + { + @this.Mode = HttpListenerMode.EmbedIO; + return @this; + } + + /// + /// Sets the type of HTTP listener to . + /// + /// The on which this method is called. + /// with its Mode property + /// set to . + /// is . + /// The configuration of is locked. + public static WebServerOptions WithMicrosoftHttpListener(this WebServerOptions @this) + { + @this.Mode = HttpListenerMode.Microsoft; + return @this; + } + + /// + /// Sets the X.509 certificate to use for SSL connections. + /// + /// The on which this method is called. + /// The X.509 certificate to use for SSL connections. + /// with its Certificate property + /// set to . + /// is . + /// The configuration of is locked. + public static WebServerOptions WithCertificate(this WebServerOptions @this, X509Certificate2 value) + { + @this.Certificate = value; + return @this; + } + + /// + /// Sets the thumbprint of the X.509 certificate to use for SSL connections. + /// + /// The on which this method is called. + /// The thumbprint of the X.509 certificate to use for SSL connections. + /// with its CertificateThumbprint property + /// set to . + /// is . + /// The configuration of is locked. + public static WebServerOptions WithCertificateThumbprint(this WebServerOptions @this, string value) + { + @this.CertificateThumbprint = value; + return @this; + } + + /// + /// Sets a value indicating whether to automatically load the X.509 certificate. + /// + /// The on which this method is called. + /// If , automatically load the X.509 certificate. + /// with its AutoLoadCertificate property + /// set to . + /// is . + /// The configuration of is locked. + /// is + /// and the underlying operating system is not Windows. + public static WebServerOptions WithAutoLoadCertificate(this WebServerOptions @this, bool value) + { + @this.AutoLoadCertificate = value; + return @this; + } + + /// + /// Instructs a instance to automatically load the X.509 certificate. + /// + /// The on which this method is called. + /// with its AutoLoadCertificate property + /// set to . + /// is . + /// The configuration of is locked. + /// The underlying operating system is not Windows. + public static WebServerOptions WithAutoLoadCertificate(this WebServerOptions @this) + { + @this.AutoLoadCertificate = true; + return @this; + } + + /// + /// Instructs a instance to not load the X.509 certificate automatically . + /// + /// The on which this method is called. + /// with its AutoLoadCertificate property + /// set to . + /// is . + /// The configuration of is locked. + public static WebServerOptions WithoutAutoLoadCertificate(this WebServerOptions @this) + { + @this.AutoLoadCertificate = false; + return @this; + } + + /// + /// Sets a value indicating whether to automatically bind the X.509 certificate + /// to the port used for HTTPS. + /// + /// The on which this method is called. + /// If , automatically bind the X.509 certificate + /// to the port used for HTTPS. + /// with its AutoRegisterCertificate property + /// set to . + /// is . + /// The configuration of is locked. + /// is + /// and the underlying operating system is not Windows. + public static WebServerOptions WithAutoRegisterCertificate(this WebServerOptions @this, bool value) + { + @this.AutoRegisterCertificate = value; + return @this; + } + + /// + /// Instructs a instance to automatically bind the X.509 certificate + /// to the port used for HTTPS. + /// + /// The on which this method is called. + /// with its AutoRegisterCertificate property + /// set to . + /// is . + /// The configuration of is locked. + /// The underlying operating system is not Windows. + public static WebServerOptions WithAutoRegisterCertificate(this WebServerOptions @this) + { + @this.AutoRegisterCertificate = true; + return @this; + } + + /// + /// Instructs a instance to not bind the X.509 certificate automatically. + /// + /// The on which this method is called. + /// with its AutoRegisterCertificate property + /// set to . + /// is . + /// The configuration of is locked. + public static WebServerOptions WithoutAutoRegisterCertificate(this WebServerOptions @this) + { + @this.AutoRegisterCertificate = false; + return @this; + } + + /// + /// Sets a value indicating the X.509 certificate store where to load the certificate from. + /// + /// The on which this method is called. + /// One of the constants. + /// with its StoreName property + /// set to . + /// is . + /// The configuration of is locked. + /// + public static WebServerOptions WithStoreName(this WebServerOptions @this, StoreName value) + { + @this.StoreName = value; + return @this; + } + + /// + /// Sets a value indicating the location of the X.509 certificate store where to load the certificate from. + /// + /// The on which this method is called. + /// One of the constants. + /// with its StoreLocation property + /// set to . + /// is . + /// The configuration of is locked. + /// + public static WebServerOptions WithStoreLocation(this WebServerOptions @this, StoreLocation value) + { + @this.StoreLocation = value; + return @this; + } + + /// + /// Sets the name and location of the X.509 certificate store where to load the certificate from. + /// + /// The on which this method is called. + /// One of the constants. + /// One of the constants. + /// with its StoreName property + /// set to and its StoreLocation property + /// set to . + /// is . + /// The configuration of is locked. + /// + /// + public static WebServerOptions WithStore(this WebServerOptions @this, StoreName name, StoreLocation location) + { + @this.StoreName = name; + @this.StoreLocation = location; + return @this; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebServerState.cs b/Vendor/EmbedIO-3.5.2/WebServerState.cs new file mode 100644 index 0000000..5f05481 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebServerState.cs @@ -0,0 +1,36 @@ +namespace EmbedIO +{ + // NOTE TO CONTRIBUTORS: + // ===================== + // Do not reorder fields or change their values. + // It is important that WebServerState values represent, + // in ascending order, the stages of a web server's lifetime, + // so that comparisons can be made; for example, + // State < WebServerState.Listening means "not yet ready to accept requests". + + /// + /// Represents the state of a web server. + /// + public enum WebServerState + { + /// + /// The web server has not been started yet. + /// + Created, + + /// + /// The web server has been started but it is still initializing. + /// + Loading, + + /// + /// The web server is ready to accept incoming requests. + /// + Listening, + + /// + /// The web server has been stopped. + /// + Stopped, + } +} diff --git a/Vendor/EmbedIO-3.5.2/WebServerStateChangedEventArgs.cs b/Vendor/EmbedIO-3.5.2/WebServerStateChangedEventArgs.cs new file mode 100644 index 0000000..45382b1 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebServerStateChangedEventArgs.cs @@ -0,0 +1,31 @@ +using System; + +namespace EmbedIO +{ + /// + /// Represents event arguments whenever the state of a web server changes. + /// + public class WebServerStateChangedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The old state. + /// The new state. + public WebServerStateChangedEventArgs(WebServerState oldState, WebServerState newState) + { + OldState = oldState; + NewState = newState; + } + + /// + /// Gets the state to which the application service changed. + /// + public WebServerState NewState { get; } + + /// + /// Gets the old state. + /// + public WebServerState OldState { get; } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebServerStateChangedEventHandler.cs b/Vendor/EmbedIO-3.5.2/WebServerStateChangedEventHandler.cs new file mode 100644 index 0000000..43b5429 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebServerStateChangedEventHandler.cs @@ -0,0 +1,9 @@ +namespace EmbedIO +{ + /// + /// An event handler that is called whenever the state of a web server is changed. + /// + /// The sender. + /// The instance containing the event data. + public delegate void WebServerStateChangedEventHandler(object sender, WebServerStateChangedEventArgs e); +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/CloseStatusCode.cs b/Vendor/EmbedIO-3.5.2/WebSockets/CloseStatusCode.cs new file mode 100644 index 0000000..e04e579 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/CloseStatusCode.cs @@ -0,0 +1,97 @@ +namespace EmbedIO.WebSockets +{ + /// + /// Indicates the status code for the WebSocket connection close. + /// + /// + /// + /// The values of this enumeration are defined in + /// Section 7.4 of RFC 6455. + /// + /// + /// "Reserved value" must not be set as a status code in a connection close frame by + /// an endpoint. It's designated for use in applications expecting a status code to + /// indicate that the connection was closed due to the system grounds. + /// + /// + public enum CloseStatusCode : ushort + { + /// + /// Equivalent to close status 1000. Indicates normal close. + /// + Normal = 1000, + + /// + /// Equivalent to close status 1001. Indicates that an endpoint is going away. + /// + Away = 1001, + + /// + /// Equivalent to close status 1002. Indicates that an endpoint is terminating + /// the connection due to a protocol error. + /// + ProtocolError = 1002, + + /// + /// Equivalent to close status 1003. Indicates that an endpoint is terminating + /// the connection because it has received a type of data that it cannot accept. + /// + UnsupportedData = 1003, + + /// + /// Equivalent to close status 1004. Still undefined. A Reserved value. + /// + Undefined = 1004, + + /// + /// Equivalent to close status 1005. Indicates that no status code was actually present. + /// A Reserved value. + /// + NoStatus = 1005, + + /// + /// Equivalent to close status 1006. Indicates that the connection was closed abnormally. + /// A Reserved value. + /// + Abnormal = 1006, + + /// + /// Equivalent to close status 1007. Indicates that an endpoint is terminating + /// the connection because it has received a message that contains data that + /// isn't consistent with the type of the message. + /// + InvalidData = 1007, + + /// + /// Equivalent to close status 1008. Indicates that an endpoint is terminating + /// the connection because it has received a message that violates its policy. + /// + PolicyViolation = 1008, + + /// + /// Equivalent to close status 1009. Indicates that an endpoint is terminating + /// the connection because it has received a message that is too big to process. + /// + TooBig = 1009, + + /// + /// Equivalent to close status 1010. Indicates that a client is terminating + /// the connection because it has expected the server to negotiate one or more extension, + /// but the server didn't return them in the handshake response. + /// + MandatoryExtension = 1010, + + /// + /// Equivalent to close status 1011. Indicates that a server is terminating + /// the connection because it has encountered an unexpected condition that + /// prevented it from fulfilling the request. + /// + ServerError = 1011, + + /// + /// Equivalent to close status 1015. Indicates that the connection was closed + /// due to a failure to perform a TLS handshake. A Reserved value. + /// + TlsHandshakeFailure = 1015, + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/IWebSocket.cs b/Vendor/EmbedIO-3.5.2/WebSockets/IWebSocket.cs new file mode 100644 index 0000000..789c012 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/IWebSocket.cs @@ -0,0 +1,54 @@ +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +namespace EmbedIO.WebSockets +{ + /// + /// + /// Interface to create a WebSocket implementation. + /// + /// + public interface IWebSocket : IDisposable + { + /// + /// Gets the state. + /// + /// + /// The state. + /// + WebSocketState State { get; } + + /// + /// Sends the buffer to the web socket asynchronously. + /// + /// The buffer. + /// if set to true [is text]. + /// The cancellation token. + /// + /// A task that represents the asynchronous of send data using websocket. + /// + Task SendAsync(byte[] buffer, bool isText, CancellationToken cancellationToken = default); + + /// + /// Closes the web socket asynchronously. + /// + /// The cancellation token. + /// + /// The task object representing the asynchronous operation. + /// + Task CloseAsync(CancellationToken cancellationToken = default); + + /// + /// Closes the web socket asynchronously. + /// + /// The code. + /// The comment. + /// The cancellation token. + /// + /// The task object representing the asynchronous operation. + /// + Task CloseAsync(CloseStatusCode code, string? comment = null, CancellationToken cancellationToken = default); + } +} diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/IWebSocketContext.cs b/Vendor/EmbedIO-3.5.2/WebSockets/IWebSocketContext.cs new file mode 100644 index 0000000..2de842e --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/IWebSocketContext.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net; +using System.Security.Principal; +using System.Threading; +using EmbedIO.Sessions; + +namespace EmbedIO.WebSockets +{ + /// + /// Represents the context of a WebSocket connection. + /// + public interface IWebSocketContext + { + /// + /// Gets a unique identifier for a WebSocket context. + /// + string Id { get; } + + /// + /// Gets the used to cancel operations. + /// + CancellationToken CancellationToken { get; } + + /// + /// Gets the unique identifier of the opening handshake HTTP context. + /// + string HttpContextId { get; } + + /// + /// Gets the session proxy associated with the opening handshake HTTP context. + /// + ISessionProxy Session { get; } + + /// + /// Gets the dictionary of data associated with the opening handshake HTTP context. + /// + IDictionary Items { get; } + + /// + /// Gets the server IP address and port number to which the opening handshake request is directed. + /// + IPEndPoint LocalEndPoint { get; } + + /// + /// Gets the client IP address and port number from which the opening handshake request originated. + /// + IPEndPoint RemoteEndPoint { get; } + + /// The URI requested by the WebSocket client. + Uri RequestUri { get; } + + /// The HTTP headers that were sent to the server during the opening handshake. + NameValueCollection Headers { get; } + + /// The value of the Origin HTTP header included in the opening handshake. + string Origin { get; } + + /// The value of the SecWebSocketKey HTTP header included in the opening handshake. + string WebSocketVersion { get; } + + /// The list of subprotocols requested by the WebSocket client. + IEnumerable RequestedProtocols { get; } + + /// The accepted subprotocol. + string AcceptedProtocol { get; } + + /// The cookies that were passed to the server during the opening handshake. + ICookieCollection Cookies { get; } + + /// An object used to obtain identity, authentication information, and security roles for the WebSocket client. + IPrincipal User { get; } + + /// Whether the WebSocket client is authenticated. + bool IsAuthenticated { get; } + + /// Whether the WebSocket client connected from the local machine. + bool IsLocal { get; } + + /// Whether the WebSocket connection is secured using Secure Sockets Layer (SSL). + bool IsSecureConnection { get; } + + /// The interface used to interact with the WebSocket connection. + IWebSocket WebSocket { get; } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/IWebSocketReceiveResult.cs b/Vendor/EmbedIO-3.5.2/WebSockets/IWebSocketReceiveResult.cs new file mode 100644 index 0000000..af0cf31 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/IWebSocketReceiveResult.cs @@ -0,0 +1,32 @@ +namespace EmbedIO.WebSockets +{ + /// + /// Interface for WebSocket Receive Result object. + /// + public interface IWebSocketReceiveResult + { + /// + /// Gets the count. + /// + /// + /// The count. + /// + int Count { get; } + + /// + /// Gets a value indicating whether [end of message]. + /// + /// + /// true if [end of message]; otherwise, false. + /// + bool EndOfMessage { get; } + + /// + /// Gets the type of the message. + /// + /// + /// The type of the message. + /// + int MessageType { get; } + } +} diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Internal/Fin.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/Fin.cs new file mode 100644 index 0000000..920d0f2 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/Fin.cs @@ -0,0 +1,22 @@ +namespace EmbedIO.WebSockets.Internal +{ + /// + /// Indicates whether a WebSocket frame is the final frame of a message. + /// + /// + /// The values of this enumeration are defined in + /// Section 5.2 of RFC 6455. + /// + internal enum Fin : byte + { + /// + /// Equivalent to numeric value 0. Indicates more frames of a message follow. + /// + More = 0x0, + + /// + /// Equivalent to numeric value 1. Indicates the final frame of a message. + /// + Final = 0x1, + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Internal/FragmentBuffer.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/FragmentBuffer.cs new file mode 100644 index 0000000..574b047 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/FragmentBuffer.cs @@ -0,0 +1,29 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace EmbedIO.WebSockets.Internal +{ + internal class FragmentBuffer : MemoryStream + { + private readonly bool _fragmentsCompressed; + private readonly Opcode _fragmentsOpcode; + + public FragmentBuffer(Opcode frameOpcode, bool frameIsCompressed) + { + _fragmentsOpcode = frameOpcode; + _fragmentsCompressed = frameIsCompressed; + } + + public void AddPayload(MemoryStream data) => data.CopyTo(this, 1024); + + public async Task GetMessage(CompressionMethod compression) + { + var data = _fragmentsCompressed + ? await this.CompressAsync(compression, false, CancellationToken.None).ConfigureAwait(false) + : this; + + return new MessageEventArgs(_fragmentsOpcode, data.ToArray()); + } + } +} diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Internal/Mask.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/Mask.cs new file mode 100644 index 0000000..c6e4b11 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/Mask.cs @@ -0,0 +1,22 @@ +namespace EmbedIO.WebSockets.Internal +{ + /// + /// Indicates whether the payload data of a WebSocket frame is masked. + /// + /// + /// The values of this enumeration are defined in + /// Section 5.2 of RFC 6455. + /// + internal enum Mask : byte + { + /// + /// Equivalent to numeric value 0. Indicates not masked. + /// + Off = 0x0, + + /// + /// Equivalent to numeric value 1. Indicates masked. + /// + On = 0x1, + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Internal/MessageEventArgs.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/MessageEventArgs.cs new file mode 100644 index 0000000..9fb383c --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/MessageEventArgs.cs @@ -0,0 +1,114 @@ +using System; +using Swan; + +namespace EmbedIO.WebSockets.Internal +{ + /// + /// Represents the event data for the event. + /// + /// + /// + /// That event occurs when the receives + /// a message or a ping if the + /// property is set to true. + /// + /// + /// If you would like to get the message data, you should access + /// the or property. + /// + /// + internal class MessageEventArgs : EventArgs + { + private readonly byte[] _rawData; + private string? _data; + private bool _dataSet; + + internal MessageEventArgs(WebSocketFrame frame) + { + Opcode = frame.Opcode; + _rawData = frame.PayloadData.ApplicationData.ToArray(); + } + + internal MessageEventArgs(Opcode opcode, byte[] rawData) + { + if ((ulong)rawData.Length > PayloadData.MaxLength) + throw new WebSocketException(CloseStatusCode.TooBig); + + Opcode = opcode; + _rawData = rawData; + } + + /// + /// Gets the message data as a . + /// + /// + /// A that represents the message data if its type is + /// text or ping and if decoding it to a string has successfully done; + /// otherwise, . + /// + public string? Data + { + get + { + SetData(); + return _data; + } + } + + /// + /// Gets a value indicating whether the message type is binary. + /// + /// + /// true if the message type is binary; otherwise, false. + /// + public bool IsBinary => Opcode == Opcode.Binary; + + /// + /// Gets a value indicating whether the message type is ping. + /// + /// + /// true if the message type is ping; otherwise, false. + /// + public bool IsPing => Opcode == Opcode.Ping; + + /// + /// Gets a value indicating whether the message type is text. + /// + /// + /// true if the message type is text; otherwise, false. + /// + public bool IsText => Opcode == Opcode.Text; + + /// + /// Gets the message data as an array of . + /// + /// + /// An array of that represents the message data. + /// + public byte[] RawData + { + get + { + SetData(); + return _rawData; + } + } + + internal Opcode Opcode { get; } + + private void SetData() + { + if (_dataSet) + return; + + if (Opcode == Opcode.Binary) + { + _dataSet = true; + return; + } + + _data = _rawData.ToText(); + _dataSet = true; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Internal/PayloadData.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/PayloadData.cs new file mode 100644 index 0000000..271c005 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/PayloadData.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Swan; +using EmbedIO.Net.Internal; + +namespace EmbedIO.WebSockets.Internal +{ + internal class PayloadData + { + public const ulong MaxLength = long.MaxValue; + + private readonly byte[] _data; + private ushort? _code; + + internal PayloadData(byte[] data) + { + _data = data; + } + + internal PayloadData(ushort code = 1005, string? reason = null) + { + _code = code; + _data = code == 1005 ? Array.Empty() : Append(code, reason); + } + + internal MemoryStream ApplicationData => new MemoryStream(_data); + + internal ulong Length => (ulong)_data.Length; + + internal ushort Code + { + get + { + if (!_code.HasValue) + { + _code = _data.Length > 1 + ? BitConverter.ToUInt16(_data.Take(2).ToArray().ToHostOrder(Endianness.Big), 0) + : (ushort)1005; + } + + return _code.Value; + } + } + + internal bool HasReservedCode => _data.Length > 1 && (Code == (ushort)CloseStatusCode.Undefined || + Code == (ushort)CloseStatusCode.NoStatus || + Code == (ushort)CloseStatusCode.Abnormal || + Code == (ushort)CloseStatusCode.TlsHandshakeFailure); + + public override string ToString() => BitConverter.ToString(_data); + + internal static byte[] Append(ushort code, string? reason) + { + var ret = code.ToByteArray(Endianness.Big); + if (string.IsNullOrEmpty(reason)) return ret; + + var buff = new List(ret); + buff.AddRange(Encoding.UTF8.GetBytes(reason)); + + return buff.ToArray(); + } + + internal void Mask(byte[] key) + { + for (long i = 0; i < _data.Length; i++) + _data[i] = (byte)(_data[i] ^ key[i % 4]); + } + + internal byte[] ToArray() => _data; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Internal/Rsv.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/Rsv.cs new file mode 100644 index 0000000..f953c60 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/Rsv.cs @@ -0,0 +1,22 @@ +namespace EmbedIO.WebSockets.Internal +{ + /// + /// Indicates whether each RSV (RSV1, RSV2, and RSV3) of a WebSocket frame is non-zero. + /// + /// + /// The values of this enumeration are defined in + /// Section 5.2 of RFC 6455. + /// + internal enum Rsv : byte + { + /// + /// Equivalent to numeric value 0. Indicates zero. + /// + Off = 0x0, + + /// + /// Equivalent to numeric value 1. Indicates non-zero. + /// + On = 0x1, + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Internal/StreamExtensions.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/StreamExtensions.cs new file mode 100644 index 0000000..83c6a43 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/StreamExtensions.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Threading; +using System.Threading.Tasks; + +namespace EmbedIO.WebSockets.Internal +{ + internal static class StreamExtensions + { + private static readonly byte[] LastByte = { 0x00 }; + + // Compresses or decompresses a stream using the specified compression method. + public static async Task CompressAsync( + this Stream @this, + CompressionMethod method, + bool compress, + CancellationToken cancellationToken) + { + @this.Position = 0; + var targetStream = new MemoryStream(); + + switch (method) + { + case CompressionMethod.Deflate: + if (compress) + { + using var compressor = new DeflateStream(targetStream, CompressionMode.Compress, true); + await @this.CopyToAsync(compressor, 1024, cancellationToken).ConfigureAwait(false); + await @this.CopyToAsync(compressor).ConfigureAwait(false); + + // WebSocket use this + targetStream.Write(LastByte, 0, 1); + targetStream.Position = 0; + } + else + { + using var compressor = new DeflateStream(@this, CompressionMode.Decompress); + await compressor.CopyToAsync(targetStream).ConfigureAwait(false); + } + + break; + case CompressionMethod.Gzip: + if (compress) + { + using var compressor = new GZipStream(targetStream, CompressionMode.Compress, true); + await @this.CopyToAsync(compressor).ConfigureAwait(false); + } + else + { + using var compressor = new GZipStream(@this, CompressionMode.Decompress); + await compressor.CopyToAsync(targetStream).ConfigureAwait(false); + } + + break; + case CompressionMethod.None: + await @this.CopyToAsync(targetStream).ConfigureAwait(false); + break; + default: + throw new ArgumentOutOfRangeException(nameof(method), method, null); + } + + return targetStream; + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Internal/SystemWebSocket.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/SystemWebSocket.cs new file mode 100644 index 0000000..8f56594 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/SystemWebSocket.cs @@ -0,0 +1,66 @@ +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +namespace EmbedIO.WebSockets.Internal +{ + internal sealed class SystemWebSocket : IWebSocket + { + public SystemWebSocket(System.Net.WebSockets.WebSocket webSocket) + { + UnderlyingWebSocket = webSocket; + } + + ~SystemWebSocket() + { + Dispose(false); + } + + public System.Net.WebSockets.WebSocket UnderlyingWebSocket { get; } + + public WebSocketState State => UnderlyingWebSocket.State; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public Task SendAsync(byte[] buffer, bool isText, CancellationToken cancellationToken = default) + => UnderlyingWebSocket.SendAsync( + new ArraySegment(buffer), + isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary, + true, + cancellationToken); + + /// + public Task CloseAsync(CancellationToken cancellationToken = default) => + UnderlyingWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken); + + /// + public Task CloseAsync(CloseStatusCode code, string? comment = null, CancellationToken cancellationToken = default)=> + UnderlyingWebSocket.CloseAsync(MapCloseStatus(code), comment ?? string.Empty, cancellationToken); + + private void Dispose(bool disposing) + { + if (!disposing) + return; + + UnderlyingWebSocket.Dispose(); + } + + private WebSocketCloseStatus MapCloseStatus(CloseStatusCode code) => code switch { + CloseStatusCode.Normal => WebSocketCloseStatus.NormalClosure, + CloseStatusCode.ProtocolError => WebSocketCloseStatus.ProtocolError, + CloseStatusCode.InvalidData => WebSocketCloseStatus.InvalidPayloadData, + CloseStatusCode.UnsupportedData => WebSocketCloseStatus.InvalidPayloadData, + CloseStatusCode.PolicyViolation => WebSocketCloseStatus.PolicyViolation, + CloseStatusCode.TooBig => WebSocketCloseStatus.MessageTooBig, + CloseStatusCode.MandatoryExtension => WebSocketCloseStatus.MandatoryExtension, + CloseStatusCode.ServerError => WebSocketCloseStatus.InternalServerError, + _ => throw new ArgumentOutOfRangeException(nameof(code), code, null) + }; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Internal/SystemWebSocketReceiveResult.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/SystemWebSocketReceiveResult.cs new file mode 100644 index 0000000..98ce67f --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/SystemWebSocketReceiveResult.cs @@ -0,0 +1,29 @@ +namespace EmbedIO.WebSockets.Internal +{ + /// + /// Represents a wrapper around a regular WebSocketContext. + /// + /// + internal sealed class SystemWebSocketReceiveResult : IWebSocketReceiveResult + { + private readonly System.Net.WebSockets.WebSocketReceiveResult _results; + + /// + /// Initializes a new instance of the class. + /// + /// The results. + public SystemWebSocketReceiveResult(System.Net.WebSockets.WebSocketReceiveResult results) + { + _results = results; + } + + /// + public int Count => _results.Count; + + /// + public bool EndOfMessage=> _results.EndOfMessage; + + /// + public int MessageType => (int) _results.MessageType; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocket.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocket.cs new file mode 100644 index 0000000..6595c5f --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocket.cs @@ -0,0 +1,561 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Net.WebSockets; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Net.Internal; +using Swan; +using Swan.Logging; + +namespace EmbedIO.WebSockets.Internal +{ + /// + /// Implements the WebSocket interface. + /// + /// + /// The WebSocket class provides a set of methods and properties for two-way communication using + /// the WebSocket protocol (RFC 6455). + /// + internal sealed class WebSocket : IWebSocket + { + public const string SupportedVersion = "13"; + + private readonly object _stateSyncRoot = new (); + private readonly ConcurrentQueue _messageEventQueue = new (); + private readonly Action _closeConnection; + private readonly TimeSpan _waitTime = TimeSpan.FromSeconds(1); + + private volatile WebSocketState _readyState; + private AutoResetEvent? _exitReceiving; + private FragmentBuffer? _fragmentsBuffer; + private volatile bool _inMessage; + private AutoResetEvent? _receivePong; + private Stream? _stream; + + private WebSocket(HttpConnection connection) + { + _closeConnection = connection.ForceClose; + _stream = connection.Stream; + _readyState = WebSocketState.Open; + } + + ~WebSocket() + { + Dispose(false); + } + + /// + /// Occurs when the receives a message. + /// + public event EventHandler? OnMessage; + + /// + public WebSocketState State => _readyState; + + internal CompressionMethod Compression { get; } = CompressionMethod.None; + + internal bool EmitOnPing { get; set; } + + internal bool InContinuation { get; private set; } + + /// + public Task SendAsync(byte[] buffer, bool isText, CancellationToken cancellationToken) => SendAsync(buffer, isText ? Opcode.Text : Opcode.Binary, cancellationToken); + + /// + public Task CloseAsync(CancellationToken cancellationToken = default) => CloseAsync(CloseStatusCode.Normal, cancellationToken: cancellationToken); + + /// + public Task CloseAsync( + CloseStatusCode code = CloseStatusCode.Undefined, + string? reason = null, + CancellationToken cancellationToken = default) + { + bool CheckParametersForClose() + { + if (code == CloseStatusCode.NoStatus && !string.IsNullOrEmpty(reason)) + { + "'code' cannot have a reason.".Trace(nameof(WebSocket)); + return false; + } + + if (code == CloseStatusCode.MandatoryExtension) + { + "'code' cannot be used by a server.".Trace(nameof(WebSocket)); + return false; + } + + if (!string.IsNullOrEmpty(reason) && Encoding.UTF8.GetBytes(reason).Length > 123) + { + "The size of 'reason' is greater than the allowable max size.".Trace(nameof(WebSocket)); + return false; + } + + return true; + } + + if (_readyState != WebSocketState.Open) + { + return Task.CompletedTask; + } + + if (code != CloseStatusCode.Undefined && !CheckParametersForClose()) + { + return Task.CompletedTask; + } + + if (code == CloseStatusCode.NoStatus) + { + return InternalCloseAsync(cancellationToken: cancellationToken); + } + + var send = !IsOpcodeReserved(code); + return InternalCloseAsync(new PayloadData((ushort)code, reason), send, send, cancellationToken); + } + + /// + /// Sends a ping using the WebSocket connection. + /// + /// + /// true if the receives a pong to this ping in a time; + /// otherwise, false. + /// + public Task PingAsync() => PingAsync(WebSocketFrame.EmptyPingBytes, _waitTime); + + /// + /// Sends a ping with the specified using the WebSocket connection. + /// + /// + /// true if the receives a pong to this ping in a time; + /// otherwise, false. + /// + /// + /// A that represents the message to send. + /// + public Task PingAsync(string message) + { + if (string.IsNullOrEmpty(message)) + { + return PingAsync(); + } + + var data = Encoding.UTF8.GetBytes(message); + + if (data.Length <= 125) + { + return PingAsync(WebSocketFrame.CreatePingFrame(data).ToArray(), _waitTime); + } + + "A message has greater than the allowable max size.".Error(nameof(PingAsync)); + + return Task.FromResult(false); + } + + /// + /// Sends binary using the WebSocket connection. + /// + /// An array of that represents the binary data to send. + /// The opcode. + /// The cancellation token. + /// + /// A task that represents the asynchronous of send + /// binary data using websocket. + /// +#pragma warning disable CA1801 // Unused parameter + public async Task SendAsync(byte[] data, Opcode opcode, CancellationToken cancellationToken = default) +#pragma warning restore CA1801 + { + if (_readyState != WebSocketState.Open) + { + throw new WebSocketException(CloseStatusCode.Normal, $"This operation isn\'t available in: {_readyState}"); + } + + using var stream = new WebSocketStream(data, opcode, Compression); + foreach (var frame in stream.GetFrames()) + { + await Send(frame).ConfigureAwait(false); + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + internal static async Task AcceptAsync(HttpListenerContext httpContext, string acceptedProtocol) + { + static string CreateResponseKey(string clientKey) + { + const string Guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + var buff = new StringBuilder(clientKey, 64).Append(Guid); +#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms + using var sha1 = SHA1.Create(); + return Convert.ToBase64String(sha1.ComputeHash(Encoding.UTF8.GetBytes(buff.ToString()))); +#pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms + } + + var requestHeaders = httpContext.Request.Headers; + + var webSocketKey = requestHeaders[HttpHeaderNames.SecWebSocketKey]; + + if (string.IsNullOrEmpty(webSocketKey)) + { + throw new WebSocketException(CloseStatusCode.ProtocolError, $"Includes no {HttpHeaderNames.SecWebSocketKey} header, or it has an invalid value."); + } + + var webSocketVersion = requestHeaders[HttpHeaderNames.SecWebSocketVersion]; + + if (webSocketVersion == null || webSocketVersion != SupportedVersion) + { + throw new WebSocketException(CloseStatusCode.ProtocolError, $"Includes no {HttpHeaderNames.SecWebSocketVersion} header, or it has an invalid value."); + } + + var handshakeResponse = new WebSocketHandshakeResponse(httpContext); + + handshakeResponse.Headers[HttpHeaderNames.SecWebSocketAccept] = CreateResponseKey(webSocketKey); + + if (acceptedProtocol.Length > 0) + { + handshakeResponse.Headers[HttpHeaderNames.SecWebSocketProtocol] = acceptedProtocol; + } + + var bytes = Encoding.UTF8.GetBytes(handshakeResponse.ToString()); + await httpContext.Connection.Stream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + + // Signal the original response that headers have been sent. + httpContext.HttpListenerResponse.HeadersSent = true; + + var socket = new WebSocket(httpContext.Connection); + socket.Open(); + return socket; + } + + internal async Task PingAsync(byte[] frameAsBytes, TimeSpan timeout) + { + if (_readyState != WebSocketState.Open) + { + return false; + } + + await _stream.WriteAsync(frameAsBytes, 0, frameAsBytes.Length).ConfigureAwait(false); + + return _receivePong != null && _receivePong.WaitOne(timeout); + } + + private static bool IsOpcodeReserved(CloseStatusCode code) + => code == CloseStatusCode.Undefined + || code == CloseStatusCode.NoStatus + || code == CloseStatusCode.Abnormal + || code == CloseStatusCode.TlsHandshakeFailure; + +#pragma warning disable CA1801 // Unused parameter + private void Dispose(bool disposing) +#pragma warning restore CA1801 + { + try + { + InternalCloseAsync(new PayloadData((ushort)CloseStatusCode.Away)).Await(); + } + catch + { + // Ignored + } + } + + private async Task InternalCloseAsync( + PayloadData? payloadData = null, + bool send = true, + bool receive = true, + CancellationToken cancellationToken = default) + { + lock (_stateSyncRoot) + { + if (_readyState == WebSocketState.CloseReceived || _readyState == WebSocketState.CloseSent) + { + "The closing is already in progress.".Trace(nameof(InternalCloseAsync)); + return; + } + + if (_readyState == WebSocketState.Closed) + { + "The connection has been closed.".Trace(nameof(InternalCloseAsync)); + return; + } + + send = send && _readyState == WebSocketState.Open; + receive = receive && send; + + _readyState = WebSocketState.CloseSent; + } + + "Begin closing the connection.".Trace(nameof(InternalCloseAsync)); + + var bytes = send ? WebSocketFrame.CreateCloseFrame(payloadData).ToArray() : null; + await CloseHandshakeAsync(bytes, receive, cancellationToken).ConfigureAwait(false); + ReleaseResources(); + + "End closing the connection.".Trace(nameof(InternalCloseAsync)); + + lock (_stateSyncRoot) + { + _readyState = WebSocketState.Closed; + } + } + + private async Task CloseHandshakeAsync( + byte[]? frameAsBytes, + bool receive, + CancellationToken cancellationToken) + { + var sent = frameAsBytes != null; + + if (sent) + { + await _stream.WriteAsync(frameAsBytes, 0, frameAsBytes.Length, cancellationToken).ConfigureAwait(false); + } + + if (receive && sent) + { + _ = _exitReceiving?.WaitOne(_waitTime); + } + } + + private void Fatal(string message, Exception? exception = null) + => Fatal(message, (exception as WebSocketException)?.Code ?? CloseStatusCode.Abnormal); + + private void Fatal(string message, CloseStatusCode code) + => InternalCloseAsync(new PayloadData((ushort)code, message), !IsOpcodeReserved(code), false).Await(); + + private void Message() + { + if (_inMessage || _messageEventQueue.IsEmpty || _readyState != WebSocketState.Open) + { + return; + } + + _inMessage = true; + + if (_messageEventQueue.TryDequeue(out var e)) + { + Messages(e); + } + } + + private void Messages(MessageEventArgs e) + { + try + { + OnMessage?.Invoke(this, e); + } + catch (Exception ex) + { + ex.Log(nameof(WebSocket)); + } + + if (!_messageEventQueue.TryDequeue(out e) || _readyState != WebSocketState.Open) + { + _inMessage = false; + return; + } + + _ = Task.Run(() => Messages(e)); + } + + private void Open() + { + _inMessage = true; + StartReceiving(); + + if (!_messageEventQueue.TryDequeue(out var e) || _readyState != WebSocketState.Open) + { + _inMessage = false; + return; + } + + Messages(e); + } + + private Task ProcessCloseFrame(WebSocketFrame frame) => InternalCloseAsync(frame.PayloadData, !frame.PayloadData.HasReservedCode, false); + + private async Task ProcessDataFrame(WebSocketFrame frame) + { + if (frame.IsCompressed) + { + using var ms = await frame.PayloadData.ApplicationData.CompressAsync(Compression, false, CancellationToken.None).ConfigureAwait(false); + + _messageEventQueue.Enqueue(new MessageEventArgs(frame.Opcode, ms.ToArray())); + } + else + { + _messageEventQueue.Enqueue(new MessageEventArgs(frame)); + } + } + + private async Task ProcessFragmentFrame(WebSocketFrame frame) + { + if (!InContinuation) + { + // Must process first fragment. + if (frame.Opcode == Opcode.Cont) + { + return; + } + + _fragmentsBuffer = new FragmentBuffer(frame.Opcode, frame.IsCompressed); + InContinuation = true; + } + + _fragmentsBuffer.AddPayload(frame.PayloadData.ApplicationData); + + if (frame.Fin == Fin.Final) + { + using (_fragmentsBuffer) + { + _messageEventQueue.Enqueue(await _fragmentsBuffer.GetMessage(Compression).ConfigureAwait(false)); + } + + _fragmentsBuffer = null; + InContinuation = false; + } + } + + private Task ProcessPingFrame(WebSocketFrame frame) + { + if (EmitOnPing) + { + _messageEventQueue.Enqueue(new MessageEventArgs(frame)); + } + + return Send(new WebSocketFrame(Opcode.Pong, frame.PayloadData)); + } + + private void ProcessPongFrame() + { + _ = _receivePong?.Set(); + "Received a pong.".Trace(nameof(ProcessPongFrame)); + } + + private async Task ProcessReceivedFrame(WebSocketFrame frame) + { + if (frame.IsFragment) + { + await ProcessFragmentFrame(frame).ConfigureAwait(false); + } + else + { + switch (frame.Opcode) + { + case Opcode.Text: + case Opcode.Binary: + await ProcessDataFrame(frame).ConfigureAwait(false); + break; + case Opcode.Ping: + await ProcessPingFrame(frame).ConfigureAwait(false); + break; + case Opcode.Pong: + ProcessPongFrame(); + break; + case Opcode.Close: + await ProcessCloseFrame(frame).ConfigureAwait(false); + break; + default: + Fatal($"Unsupported frame received: {frame.PrintToString()}", CloseStatusCode.PolicyViolation); + return false; + } + } + + return true; + } + + private void ReleaseResources() + { + _closeConnection(); + _stream = null; + + if (_fragmentsBuffer != null) + { + _fragmentsBuffer.Dispose(); + _fragmentsBuffer = null; + InContinuation = false; + } + + if (_receivePong != null) + { + _receivePong.Dispose(); + _receivePong = null; + } + + if (_exitReceiving == null) + { + return; + } + + _exitReceiving.Dispose(); + _exitReceiving = null; + } + + private Task Send(WebSocketFrame frame) + { + lock (_stateSyncRoot) + { + if (_readyState != WebSocketState.Open) + { + "The sending has been interrupted.".Error(nameof(Send)); + return Task.Delay(0); + } + } + + var frameAsBytes = frame.ToArray(); + return _stream.WriteAsync(frameAsBytes, 0, frameAsBytes.Length); + } + + private void StartReceiving() + { + while (_messageEventQueue.TryDequeue(out _)) + { + // do nothing + } + + _exitReceiving = new AutoResetEvent(false); + _receivePong = new AutoResetEvent(false); + + var frameStream = new WebSocketFrameStream(_stream); + + _ = Task.Run(async () => + { + while (_readyState == WebSocketState.Open) + { + try + { + var frame = await frameStream.ReadFrameAsync(this).ConfigureAwait(false); + + if (frame == null) + { + return; + } + + var result = await ProcessReceivedFrame(frame).ConfigureAwait(false); + + if (!result || _readyState == WebSocketState.Closed) + { + _ = _exitReceiving?.Set(); + + return; + } + + _ = Task.Run(Message); + } + catch (Exception ex) + { + Fatal("An exception has occurred while receiving.", ex); + } + } + }); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketContext.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketContext.cs new file mode 100644 index 0000000..14f6b20 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketContext.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net; +using System.Security.Principal; +using System.Threading; +using EmbedIO.Sessions; +using EmbedIO.Utilities; + +namespace EmbedIO.WebSockets.Internal +{ + internal sealed class WebSocketContext : IWebSocketContext + { + internal WebSocketContext( + IHttpContextImpl httpContext, + string webSocketVersion, + IEnumerable requestedProtocols, + string acceptedProtocol, + IWebSocket webSocket, + CancellationToken cancellationToken) + { + Id = UniqueIdGenerator.GetNext(); + CancellationToken = cancellationToken; + HttpContextId = httpContext.Id; + Session = httpContext.Session; + Items = httpContext.Items; + LocalEndPoint = httpContext.LocalEndPoint; + RemoteEndPoint = httpContext.RemoteEndPoint; + RequestUri = httpContext.Request.Url; + Headers = httpContext.Request.Headers; + Origin = Headers[HttpHeaderNames.Origin]; + RequestedProtocols = requestedProtocols; + AcceptedProtocol = acceptedProtocol; + WebSocketVersion = webSocketVersion; + Cookies = httpContext.Request.Cookies; + User = httpContext.User; + IsAuthenticated = httpContext.User.Identity.IsAuthenticated; + IsLocal = httpContext.Request.IsLocal; + IsSecureConnection = httpContext.Request.IsSecureConnection; + WebSocket = webSocket; + } + + /// + public string Id { get; } + + /// + public CancellationToken CancellationToken { get; } + + /// + public string HttpContextId { get; } + + /// + public ISessionProxy Session { get; } + + /// + public IDictionary Items { get; } + + /// + public IPEndPoint LocalEndPoint { get; } + + /// + public IPEndPoint RemoteEndPoint { get; } + + /// + public Uri RequestUri { get; } + + /// + public NameValueCollection Headers { get; } + + /// + public string Origin { get; } + + /// + public IEnumerable RequestedProtocols { get; } + + /// + public string AcceptedProtocol { get; } + + /// + public string WebSocketVersion { get; } + + /// + public ICookieCollection Cookies { get; } + + /// + public IPrincipal User { get; } + + /// + public bool IsAuthenticated { get; } + + /// + public bool IsLocal { get; } + + /// + public bool IsSecureConnection { get; } + + /// + public IWebSocket WebSocket { get; } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketFrame.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketFrame.cs new file mode 100644 index 0000000..c63adcc --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketFrame.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using EmbedIO.Net.Internal; +using Swan; + +namespace EmbedIO.WebSockets.Internal +{ + internal class WebSocketFrame + { + internal static readonly byte[] EmptyPingBytes = CreatePingFrame().ToArray(); + + internal WebSocketFrame(Opcode opcode, PayloadData payloadData) + : this(Fin.Final, opcode, payloadData) + { + } + + internal WebSocketFrame(Fin fin, Opcode opcode, byte[] data, bool compressed) + : this(fin, opcode, new PayloadData(data), compressed) + { + } + + internal WebSocketFrame( + Fin fin, + Opcode opcode, + PayloadData payloadData, + bool compressed = false) + { + Fin = fin; + Rsv1 = IsOpcodeData(opcode) && compressed ? Rsv.On : Rsv.Off; + Rsv2 = Rsv.Off; + Rsv3 = Rsv.Off; + Opcode = opcode; + + var len = payloadData.Length; + if (len < 126) + { + PayloadLength = (byte)len; + ExtendedPayloadLength = Array.Empty(); + } + else if (len < 0x010000) + { + PayloadLength = 126; + ExtendedPayloadLength = ((ushort)len).ToByteArray(Endianness.Big); + } + else + { + PayloadLength = 127; + ExtendedPayloadLength = len.ToByteArray(Endianness.Big); + } + + Mask = Mask.Off; + MaskingKey = Array.Empty(); + PayloadData = payloadData; + } + + internal WebSocketFrame( + Fin fin, + Rsv rsv1, + Rsv rsv2, + Rsv rsv3, + Opcode opcode, + Mask mask, + byte payloadLength) + { + Fin = fin; + Rsv1 = rsv1; + Rsv2 = rsv2; + Rsv3 = rsv3; + Opcode = opcode; + Mask = mask; + PayloadLength = payloadLength; + } + + public byte[]? ExtendedPayloadLength { get; internal set; } + + public Fin Fin { get; internal set; } + + public bool IsCompressed => Rsv1 == Rsv.On; + + public bool IsFragment => Fin == Fin.More || Opcode == Opcode.Cont; + + public bool IsMasked => Mask == Mask.On; + + public Mask Mask { get; internal set; } + + public byte[] MaskingKey { get; internal set; } + + public Opcode Opcode { get; internal set; } + + public PayloadData PayloadData { get; internal set; } + + public byte PayloadLength { get; internal set; } + + public Rsv Rsv1 { get; internal set; } + + public Rsv Rsv2 { get; internal set; } + + public Rsv Rsv3 { get; internal set; } + + internal int ExtendedPayloadLengthCount => PayloadLength < 126 ? 0 : (PayloadLength == 126 ? 2 : 8); + + internal ulong FullPayloadLength => PayloadLength < 126 + ? PayloadLength + : PayloadLength == 126 + ? BitConverter.ToUInt16(ExtendedPayloadLength.ToHostOrder(Endianness.Big), 0) + : BitConverter.ToUInt64(ExtendedPayloadLength.ToHostOrder(Endianness.Big), 0); + + public IEnumerator GetEnumerator() => ((IEnumerable)ToArray()).GetEnumerator(); + + public string PrintToString() + { + // Payload Length + var payloadLen = PayloadLength; + + // Extended Payload Length + var extPayloadLen = payloadLen > 125 ? FullPayloadLength.ToString(CultureInfo.InvariantCulture) : string.Empty; + + // Masking Key + var maskingKey = BitConverter.ToString(MaskingKey); + + // Payload Data + var payload = payloadLen == 0 + ? string.Empty + : payloadLen > 125 + ? "---" + : Opcode == Opcode.Text && !(IsFragment || IsMasked || IsCompressed) + ? PayloadData.ApplicationData.ToArray().ToText() + : PayloadData.ToString(); + + return $@" + FIN: {Fin} + RSV1: {Rsv1} + RSV2: {Rsv2} + RSV3: {Rsv3} + Opcode: {Opcode} + MASK: {Mask} + Payload Length: {payloadLen} +Extended Payload Length: {extPayloadLen} + Masking Key: {maskingKey} + Payload Data: {payload}"; + } + + public byte[] ToArray() + { + using var buff = new MemoryStream(); + var header = (int)Fin; + + header = (header << 1) + (int)Rsv1; + header = (header << 1) + (int)Rsv2; + header = (header << 1) + (int)Rsv3; + header = (header << 4) + (int)Opcode; + header = (header << 1) + (int)Mask; + header = (header << 7) + PayloadLength; + buff.Write(((ushort)header).ToByteArray(Endianness.Big), 0, 2); + + if (PayloadLength > 125) + { + buff.Write(ExtendedPayloadLength, 0, PayloadLength == 126 ? 2 : 8); + } + + if (Mask == Mask.On) + { + buff.Write(MaskingKey, 0, 4); + } + + if (PayloadLength > 0) + { + var bytes = PayloadData.ToArray(); + if (PayloadLength < 127) + { + buff.Write(bytes, 0, bytes.Length); + } + else + { + using var input = new MemoryStream(bytes); + input.CopyTo(buff, 1024); + } + } + + return buff.ToArray(); + } + + public override string ToString() => BitConverter.ToString(ToArray()); + + internal static WebSocketFrame CreateCloseFrame(PayloadData? payloadData) => new (Fin.Final, Opcode.Close, payloadData ?? new PayloadData()); + + internal static WebSocketFrame CreatePingFrame() => new (Fin.Final, Opcode.Ping, new PayloadData()); + + internal static WebSocketFrame CreatePingFrame(byte[] data) => new (Fin.Final, Opcode.Ping, new PayloadData(data)); + + internal void Validate(WebSocket webSocket) + { + if (!IsMasked) + { + throw new WebSocketException(CloseStatusCode.ProtocolError, "A frame from a client isn't masked."); + } + + if (webSocket.InContinuation && (Opcode == Opcode.Text || Opcode == Opcode.Binary)) + { + throw new WebSocketException(CloseStatusCode.ProtocolError, + "A data frame has been received while receiving continuation frames."); + } + + if (IsCompressed && webSocket.Compression == CompressionMethod.None) + { + throw new WebSocketException(CloseStatusCode.ProtocolError, + "A compressed frame has been received without any agreement for it."); + } + + if (Rsv2 == Rsv.On) + { + throw new WebSocketException(CloseStatusCode.ProtocolError, + "The RSV2 of a frame is non-zero without any negotiation for it."); + } + + if (Rsv3 == Rsv.On) + { + throw new WebSocketException(CloseStatusCode.ProtocolError, + "The RSV3 of a frame is non-zero without any negotiation for it."); + } + } + + internal void Unmask() + { + if (Mask == Mask.Off) + { + return; + } + + Mask = Mask.Off; + PayloadData.Mask(MaskingKey); + MaskingKey = Array.Empty(); + } + + private static bool IsOpcodeData(Opcode opcode) => opcode == Opcode.Text || opcode == Opcode.Binary; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketFrameStream.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketFrameStream.cs new file mode 100644 index 0000000..a5b58b3 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketFrameStream.cs @@ -0,0 +1,148 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Swan; + +namespace EmbedIO.WebSockets.Internal +{ + internal class WebSocketFrameStream + { + private readonly bool _unmask; + private readonly Stream? _stream; + + public WebSocketFrameStream(Stream? stream, bool unmask = false) + { + _stream = stream; + _unmask = unmask; + } + + internal async Task ReadFrameAsync(WebSocket webSocket) + { + if (_stream == null) return null; + + var frame = ProcessHeader(await _stream.ReadBytesAsync(2).ConfigureAwait(false)); + + await ReadExtendedPayloadLengthAsync(frame).ConfigureAwait(false); + await ReadMaskingKeyAsync(frame).ConfigureAwait(false); + await ReadPayloadDataAsync(frame).ConfigureAwait(false); + + if (_unmask) + frame.Unmask(); + + frame.Validate(webSocket); + + frame.Unmask(); + + return frame; + } + + private static bool IsOpcodeData(byte opcode) => opcode == 0x1 || opcode == 0x2; + + private static bool IsOpcodeControl(byte opcode) => opcode > 0x7 && opcode < 0x10; + + private static WebSocketFrame ProcessHeader(byte[] header) + { + if (header.Length != 2) + throw new WebSocketException("The header of a frame cannot be read from the stream."); + + // FIN + var fin = (header[0] & 0x80) == 0x80 ? Fin.Final : Fin.More; + + // RSV1 + var rsv1 = (header[0] & 0x40) == 0x40 ? Rsv.On : Rsv.Off; + + // RSV2 + var rsv2 = (header[0] & 0x20) == 0x20 ? Rsv.On : Rsv.Off; + + // RSV3 + var rsv3 = (header[0] & 0x10) == 0x10 ? Rsv.On : Rsv.Off; + + // Opcode + var opcode = (byte)(header[0] & 0x0f); + + // MASK + var mask = (header[1] & 0x80) == 0x80 ? Mask.On : Mask.Off; + + // Payload Length + var payloadLen = (byte)(header[1] & 0x7f); + + var err = !Enum.IsDefined(typeof(Opcode), opcode) ? "An unsupported opcode." + : !IsOpcodeData(opcode) && rsv1 == Rsv.On ? "A non data frame is compressed." + : IsOpcodeControl(opcode) && fin == Fin.More ? "A control frame is fragmented." + : IsOpcodeControl(opcode) && payloadLen > 125 ? "A control frame has a long payload length." + : null; + + if (err != null) + throw new WebSocketException(CloseStatusCode.ProtocolError, err); + + return new WebSocketFrame(fin, rsv1, rsv2, rsv3, (Opcode)opcode, mask, payloadLen); + } + + private async Task ReadExtendedPayloadLengthAsync(WebSocketFrame frame) + { + var len = frame.ExtendedPayloadLengthCount; + + if (len == 0) + { + frame.ExtendedPayloadLength = Array.Empty(); + return; + } + + var bytes = await _stream.ReadBytesAsync(len).ConfigureAwait(false); + + if (bytes.Length != len) + { + throw new WebSocketException( + "The extended payload length of a frame cannot be read from the stream."); + } + + frame.ExtendedPayloadLength = bytes; + } + + private async Task ReadMaskingKeyAsync(WebSocketFrame frame) + { + var len = frame.IsMasked ? 4 : 0; + + if (len == 0) + { + frame.MaskingKey = Array.Empty(); + return; + } + + var bytes = await _stream.ReadBytesAsync(len).ConfigureAwait(false); + if (bytes.Length != len) + { + throw new WebSocketException( + "The masking key of a frame cannot be read from the stream."); + } + + frame.MaskingKey = bytes; + } + + private async Task ReadPayloadDataAsync(WebSocketFrame frame) + { + var len = frame.FullPayloadLength; + if (len == 0) + { + frame.PayloadData = new PayloadData(); + + return; + } + + if (len > PayloadData.MaxLength) + throw new WebSocketException(CloseStatusCode.TooBig, "A frame has a long payload length."); + + var bytes = frame.PayloadLength < 127 + ? await _stream.ReadBytesAsync((int)len).ConfigureAwait(false) + : await _stream.ReadBytesAsync((int)len, 1024).ConfigureAwait(false); + + if (bytes.Length != (int)len) + { + throw new WebSocketException( + "The payload data of a frame cannot be read from the stream."); + } + + frame.PayloadData = new PayloadData(bytes); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketReceiveResult.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketReceiveResult.cs new file mode 100644 index 0000000..07d1b94 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketReceiveResult.cs @@ -0,0 +1,29 @@ +using System; + +namespace EmbedIO.WebSockets.Internal +{ + /// + /// Represents a WS Receive result. + /// + internal sealed class WebSocketReceiveResult : IWebSocketReceiveResult + { + internal WebSocketReceiveResult(int count, Opcode code) + { + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count)); + + Count = count; + EndOfMessage = code == Opcode.Close; + MessageType = code == Opcode.Text ? 0 : 1; + } + + /// + public int Count { get; } + + /// + public bool EndOfMessage { get; } + + /// + public int MessageType { get; } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketStream.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketStream.cs new file mode 100644 index 0000000..322ee9a --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketStream.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using Swan; + +namespace EmbedIO.WebSockets.Internal +{ + internal class WebSocketStream : MemoryStream + { + internal const int FragmentLength = 1016; + + private readonly CompressionMethod _compression; + private readonly Opcode _opcode; + + public WebSocketStream(byte[] data, Opcode opcode, CompressionMethod compression) + : base(data) + { + _compression = compression; + _opcode = opcode; + } + + public IEnumerable GetFrames() + { + var compressed = _compression != CompressionMethod.None; + var stream = compressed + ? this.CompressAsync(_compression, true, CancellationToken.None).Await() + : this; + + var len = stream.Length; + + /* Not fragmented */ + + if (len == 0) + { + yield return new WebSocketFrame(Fin.Final, _opcode, Array.Empty(), compressed); + yield break; + } + + var quo = len / FragmentLength; + var rem = (int)(len % FragmentLength); + + byte[] buff; + + if (quo == 0) + { + buff = new byte[rem]; + + if (stream.Read(buff, 0, rem) == rem) + yield return new WebSocketFrame(Fin.Final, _opcode, buff, compressed); + + yield break; + } + + buff = new byte[FragmentLength]; + if (quo == 1 && rem == 0) + { + if (stream.Read(buff, 0, FragmentLength) == FragmentLength) + yield return new WebSocketFrame(Fin.Final, _opcode, buff, compressed); + + yield break; + } + + /* Send fragmented */ + + // Begin + if (stream.Read(buff, 0, FragmentLength) != FragmentLength) + yield break; + + yield return new WebSocketFrame(Fin.More, _opcode, buff, compressed); + + var n = rem == 0 ? quo - 2 : quo - 1; + for (var i = 0; i < n; i++) + { + if (stream.Read(buff, 0, FragmentLength) != FragmentLength) + yield break; + + yield return new WebSocketFrame(Fin.More, Opcode.Cont, buff, compressed); + } + + // End + if (rem == 0) + rem = FragmentLength; + else + buff = new byte[rem]; + + if (stream.Read(buff, 0, rem) == rem) + yield return new WebSocketFrame(Fin.Final, Opcode.Cont, buff, compressed); + } + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/Opcode.cs b/Vendor/EmbedIO-3.5.2/WebSockets/Opcode.cs new file mode 100644 index 0000000..195cf53 --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/Opcode.cs @@ -0,0 +1,43 @@ +namespace EmbedIO.WebSockets +{ + /// + /// Indicates the WebSocket frame type. + /// + /// + /// The values of this enumeration are defined in + /// + /// Section 5.2 of RFC 6455. + /// + public enum Opcode : byte + { + /// + /// Equivalent to numeric value 0. Indicates continuation frame. + /// + Cont = 0x0, + + /// + /// Equivalent to numeric value 1. Indicates text frame. + /// + Text = 0x1, + + /// + /// Equivalent to numeric value 2. Indicates binary frame. + /// + Binary = 0x2, + + /// + /// Equivalent to numeric value 8. Indicates connection close frame. + /// + Close = 0x8, + + /// + /// Equivalent to numeric value 9. Indicates ping frame. + /// + Ping = 0x9, + + /// + /// Equivalent to numeric value 10. Indicates pong frame. + /// + Pong = 0xa, + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/WebSocketException.cs b/Vendor/EmbedIO-3.5.2/WebSockets/WebSocketException.cs new file mode 100644 index 0000000..d7303bf --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/WebSocketException.cs @@ -0,0 +1,51 @@ +using System; + +namespace EmbedIO.WebSockets +{ + /// + /// The exception that is thrown when a WebSocket gets a fatal error. + /// +#pragma warning disable CA1032 // Implement standard exception constructors - this class doesn't need public constructors. + public class WebSocketException : Exception +#pragma warning restore CA1032 + { + internal WebSocketException(string? message = null) + : this(CloseStatusCode.Abnormal, message) + { + // Ignore + } + + internal WebSocketException(CloseStatusCode code, Exception? innerException = null) + : this(code, null, innerException) + { + } + + internal WebSocketException(CloseStatusCode code, string? message, Exception? innerException = null) + : base(message ?? GetMessage(code), innerException) + { + Code = code; + } + + /// + /// Gets the status code indicating the cause of the exception. + /// + /// + /// One of the enum values, represents the status code + /// indicating the cause of the exception. + /// + public CloseStatusCode Code { get; } + + internal static string GetMessage(CloseStatusCode code) => code switch { + CloseStatusCode.ProtocolError => "A WebSocket protocol error has occurred.", + CloseStatusCode.UnsupportedData => "Unsupported data has been received.", + CloseStatusCode.Abnormal => "An exception has occurred.", + CloseStatusCode.InvalidData => "Invalid data has been received.", + CloseStatusCode.PolicyViolation => "A policy violation has occurred.", + CloseStatusCode.TooBig => "A too big message has been received.", + CloseStatusCode.MandatoryExtension => "WebSocket client didn't receive expected extension(s).", + CloseStatusCode.ServerError => "WebSocket server got an internal error.", + CloseStatusCode.TlsHandshakeFailure => "An error has occurred during a TLS handshake.", + _ => string.Empty + }; + } +} \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/WebSocketModule.cs b/Vendor/EmbedIO-3.5.2/WebSockets/WebSocketModule.cs new file mode 100644 index 0000000..a4cef0a --- /dev/null +++ b/Vendor/EmbedIO-3.5.2/WebSockets/WebSocketModule.cs @@ -0,0 +1,640 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using EmbedIO.Utilities; +using EmbedIO.WebSockets.Internal; +using Swan; +using Swan.Logging; +using Swan.Threading; + +namespace EmbedIO.WebSockets +{ + /// + /// A base class for modules that handle WebSocket connections. + /// + /// + /// Each WebSocket server has a list of WebSocket subprotocols it can accept. + /// When a client initiates a WebSocket opening handshake: + /// + /// if the list of accepted subprotocols is empty, + /// the connection is accepted only if no SecWebSocketProtocol + /// header is present in the request; + /// if the list of accepted subprotocols is not empty, + /// the connection is accepted only if one or more SecWebSocketProtocol + /// headers are present in the request and one of them specifies one + /// of the subprotocols in the list. The first subprotocol specified by the client + /// that is also present in the module's list is then specified in the + /// handshake response. + /// + /// If a connection is not accepted because of a subprotocol mismatch, + /// a 400 Bad Request response is sent back to the client. The response + /// contains one or more SecWebSocketProtocol headers that specify + /// the list of accepted subprotocols (if any). + /// + public abstract class WebSocketModule : WebModuleBase, IDisposable + { + private const int ReceiveBufferSize = 2048; + + private readonly bool _enableConnectionWatchdog; + private readonly List _protocols = new List(); + private readonly ConcurrentDictionary _contexts = new ConcurrentDictionary(); + private bool _isDisposing; + private int _maxMessageSize; + private TimeSpan _keepAliveInterval; + private Encoding _encoding; + private PeriodicTask? _connectionWatchdog; + + /// + /// Initializes a new instance of the class. + /// + /// The URL path of the WebSocket endpoint to serve. + /// If set to , + /// contexts representing closed connections will automatically be purged + /// from every 30 seconds.. + protected WebSocketModule(string urlPath, bool enableConnectionWatchdog) + : base(urlPath) + { + _enableConnectionWatchdog = enableConnectionWatchdog; + _maxMessageSize = 0; + _keepAliveInterval = TimeSpan.FromSeconds(30); + _encoding = Encoding.UTF8; + } + + /// + public sealed override bool IsFinalHandler => true; + + /// + /// Gets or sets the maximum size of a received message. + /// If a message exceeding the maximum size is received from a client, + /// the connection is closed automatically. + /// The default value is 0, which disables message size checking. + /// + protected int MaxMessageSize + { + get => _maxMessageSize; + set + { + EnsureConfigurationNotLocked(); + _maxMessageSize = Math.Max(value, 0); + } + } + + /// + /// Gets or sets the keep-alive interval for the WebSocket connection. + /// The default is 30 seconds. + /// + /// This property is being set to a value + /// that is too small to be acceptable. + protected TimeSpan KeepAliveInterval + { + get => _keepAliveInterval; + set + { + EnsureConfigurationNotLocked(); + if (value != Timeout.InfiniteTimeSpan && value < TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(value), "The specified keep-alive interval is too small."); + + _keepAliveInterval = value; + } + } + + /// + /// Gets the used by the method + /// to send a string. The default is per the WebSocket specification. + /// + /// This property is being set to . + protected Encoding Encoding + { + get => _encoding; + set + { + EnsureConfigurationNotLocked(); + _encoding = Validate.NotNull(nameof(value), value); + } + } + + /// + /// Gets a list of interfaces + /// representing the currently connected clients. + /// + protected IReadOnlyList ActiveContexts + { + get + { + // ConcurrentDictionary.Values, although declared as ICollection, + // will probably return a ReadOnlyCollection, which implements IReadOnlyList: + // https://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentDictionary.cs,fe55c11912af21d2 + // https://github.com/dotnet/corefx/blob/master/src/System.Collections.Concurrent/src/System/Collections/Concurrent/ConcurrentDictionary.cs#L1990 + // https://github.com/mono/mono/blob/master/mcs/class/referencesource/mscorlib/system/collections/Concurrent/ConcurrentDictionary.cs#L1961 + // However there is no formal guarantee, so be ready to convert to a list, just in case. + var values = _contexts.Values; + return values is IReadOnlyList list + ? list + : values.ToList(); + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + protected sealed override async Task OnRequestAsync(IHttpContext context) + { + // The WebSocket endpoint must match exactly, giving a RequestedPath of "/". + // In all other cases the path is longer, so there's no need to compare strings here. + if (context.RequestedPath.Length > 1) + return; + + var requestedProtocols = context.Request.Headers.GetValues(HttpHeaderNames.SecWebSocketProtocol) + ?.Select(s => s.Trim()) + .Where(s => s.Length > 0) + .ToArray() + ?? Array.Empty(); + string acceptedProtocol; + bool acceptConnection; + if (_protocols.Count > 0) + { + acceptedProtocol = requestedProtocols.FirstOrDefault(p => _protocols.Contains(p)) ?? string.Empty; + acceptConnection = acceptedProtocol.Length > 0; + } + else + { + acceptedProtocol = string.Empty; + acceptConnection = requestedProtocols.Length == 0; + } + + if (!acceptConnection) + { + $"{BaseRoute} - Rejecting WebSocket connection: no subprotocol was accepted.".Debug(nameof(WebSocketModule)); + foreach (var protocol in _protocols) + context.Response.Headers.Add(HttpHeaderNames.SecWebSocketProtocol, protocol); + + // Not throwing a HTTP exception here because a WebSocket client + // does not care about nice, formatted messages. + context.Response.SetEmptyResponse((int)HttpStatusCode.BadRequest); + return; + } + + var contextImpl = context.GetImplementation(); + $"{BaseRoute} - Accepting WebSocket connection with subprotocol \"{acceptedProtocol}\"".Debug(nameof(WebSocketModule)); + var webSocketContext = await contextImpl.AcceptWebSocketAsync( + requestedProtocols, + acceptedProtocol, + ReceiveBufferSize, + KeepAliveInterval, + context.CancellationToken).ConfigureAwait(false); + + PurgeDisconnectedContexts(); + _ = _contexts.TryAdd(webSocketContext.Id, webSocketContext); + + $"{BaseRoute} - WebSocket connection accepted - There are now {_contexts.Count} sockets connected." + .Debug(nameof(WebSocketModule)); + + await OnClientConnectedAsync(webSocketContext).ConfigureAwait(false); + + try + { + if (webSocketContext.WebSocket is SystemWebSocket systemWebSocket) + { + await ProcessSystemContext( + webSocketContext, + systemWebSocket.UnderlyingWebSocket, + context.CancellationToken).ConfigureAwait(false); + } + else + { + await ProcessEmbedIOContext(webSocketContext, context.CancellationToken) + .ConfigureAwait(false); + } + } + catch (TaskCanceledException) + { + // ignore + } + catch (Exception ex) + { + ex.Log(nameof(WebSocketModule)); + } + finally + { + // once the loop is completed or connection aborted, remove the WebSocket + RemoveWebSocket(webSocketContext); + } + } + + /// + protected override void OnStart(CancellationToken cancellationToken) + { + if (_enableConnectionWatchdog) + { + _connectionWatchdog = new PeriodicTask( + TimeSpan.FromSeconds(30), + ct => { + PurgeDisconnectedContexts(); + return Task.CompletedTask; + }, + cancellationToken); + } + } + + /// + /// Adds a WebSocket subprotocol to the list of protocols supported by a . + /// + /// The protocol name to add to the list. + /// is . + /// + /// contains one or more invalid characters, as defined + /// in RFC6455, Section 4.3. + /// - or - + /// is already in the list of supported protocols. + /// + /// The has already been started. + /// + /// + /// + protected void AddProtocol(string protocol) + { + protocol = Validate.Rfc2616Token(nameof(protocol), protocol); + + EnsureConfigurationNotLocked(); + + if (_protocols.Contains(protocol)) + throw new ArgumentException("Duplicate WebSocket protocol name.", nameof(protocol)); + + _protocols.Add(protocol); + } + + /// + /// Adds one or more WebSocket subprotocols to the list of protocols supported by a . + /// + /// The protocol names to add to the list. + /// + /// is . + /// - or - + /// One or more of the strings in is . + /// + /// + /// One or more of the strings in + /// contains one or more invalid characters, as defined + /// in RFC6455, Section 4.3. + /// - or - + /// One or more of the strings in + /// is already in the list of supported protocols. + /// + /// The has already been started. + /// + /// This method enumerates just once; hence, if an exception is thrown + /// because one of the specified protocols is or contains invalid characters, + /// any preceding protocol is added to the list of supported protocols. + /// + /// + /// + /// + protected void AddProtocols(IEnumerable protocols) + { + protocols = Validate.NotNull(nameof(protocols), protocols); + + EnsureConfigurationNotLocked(); + + foreach (var protocol in protocols.Select(p => Validate.Rfc2616Token(nameof(protocols), p))) + { + if (_protocols.Contains(protocol)) + throw new ArgumentException("Duplicate WebSocket protocol name.", nameof(protocols)); + + _protocols.Add(protocol); + } + } + + /// + /// Adds one or more WebSocket subprotocols to the list of protocols supported by a . + /// + /// The protocol names to add to the list. + /// + /// is . + /// - or - + /// One or more of the strings in is . + /// + /// + /// One or more of the strings in + /// contains one or more invalid characters, as defined + /// in RFC6455, Section 4.3. + /// - or - + /// One or more of the strings in + /// is already in the list of supported protocols. + /// + /// The has already been started. + /// + /// This method performs validation checks on all specified before adding them + /// to the list of supported protocols; hence, if an exception is thrown + /// because one of the specified protocols is or contains invalid characters, + /// none of the specified protocol names are added to the list. + /// + /// + /// + /// + protected void AddProtocols(params string[] protocols) + { + protocols = Validate.NotNull(nameof(protocols), protocols); + + if (protocols.Select(p => Validate.Rfc2616Token(nameof(protocols), p)).Any(protocol => _protocols.Contains(protocol))) + throw new ArgumentException("Duplicate WebSocket protocol name.", nameof(protocols)); + + EnsureConfigurationNotLocked(); + + _protocols.AddRange(protocols); + } + + /// + /// Sends a text payload. + /// + /// The web socket. + /// The payload. + /// A representing the ongoing operation. + protected async Task SendAsync(IWebSocketContext context, string payload) + { + try + { + var buffer = _encoding.GetBytes(payload ?? string.Empty); + + await context.WebSocket.SendAsync(buffer, true, context.CancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + ex.Log(nameof(WebSocketModule)); + } + } + +#pragma warning disable CA1822 // Member can be declared as static - It is an instance method for API consistency. + /// + /// Sends a binary payload. + /// + /// The web socket. + /// The payload. + /// A representing the ongoing operation. + protected async Task SendAsync(IWebSocketContext context, byte[] payload) + { + try + { + await context.WebSocket.SendAsync(payload ?? Array.Empty(), false, context.CancellationToken) + .ConfigureAwait(false); + } + catch (Exception ex) + { + ex.Log(nameof(WebSocketModule)); + } + } +#pragma warning restore CA1822 + + /// + /// Broadcasts the specified payload to all connected WebSocket clients. + /// + /// The payload. + /// A representing the ongoing operation. + protected Task BroadcastAsync(byte[] payload) + => Task.WhenAll(_contexts.Values.Select(c => SendAsync(c, payload))); + + /// + /// Broadcasts the specified payload to selected WebSocket clients. + /// + /// The payload. + /// A callback function that must return + /// for each context to be included in the broadcast. + /// A representing the ongoing operation. + protected Task BroadcastAsync(byte[] payload, Func selector) + => Task.WhenAll(_contexts.Values.Where(Validate.NotNull(nameof(selector), selector)).Select(c => SendAsync(c, payload))); + + /// + /// Broadcasts the specified payload to all connected WebSocket clients. + /// + /// The payload. + /// A representing the ongoing operation. + protected Task BroadcastAsync(string payload) + => Task.WhenAll(_contexts.Values.Select(c => SendAsync(c, payload))); + + /// + /// Broadcasts the specified payload to selected WebSocket clients. + /// + /// The payload. + /// A callback function that must return + /// for each context to be included in the broadcast. + /// A representing the ongoing operation. + protected Task BroadcastAsync(string payload, Func selector) + => Task.WhenAll(_contexts.Values.Where(Validate.NotNull(nameof(selector), selector)).Select(c => SendAsync(c, payload))); + + /// + /// Closes the specified web socket, removes it and disposes it. + /// + /// The web socket. + /// A representing the ongoing operation. + protected async Task CloseAsync(IWebSocketContext context) + { + if (context == null) + return; + + try + { + await context.WebSocket.CloseAsync(context.CancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + ex.Log(nameof(WebSocketModule)); + } + finally + { + RemoveWebSocket(context); + } + } + + /// + /// Called when this WebSocket server receives a full message (EndOfMessage) from a client. + /// + /// The context. + /// The buffer. + /// The result. + /// A representing the ongoing operation. + protected abstract Task OnMessageReceivedAsync(IWebSocketContext context, byte[] buffer, IWebSocketReceiveResult result); + + /// + /// Called when this WebSocket server receives a message frame regardless if the frame represents the EndOfMessage. + /// + /// The context. + /// The buffer. + /// The result. + /// A representing the ongoing operation. + protected virtual Task OnFrameReceivedAsync( + IWebSocketContext context, + byte[] buffer, + IWebSocketReceiveResult result) + => Task.CompletedTask; + + /// + /// Called when this WebSocket server accepts a new client. + /// + /// The context. + /// A representing the ongoing operation. + protected virtual Task OnClientConnectedAsync(IWebSocketContext context) => Task.CompletedTask; + + /// + /// Called when the server has removed a connected client for any reason. + /// + /// The context. + /// A representing the ongoing operation. + protected virtual Task OnClientDisconnectedAsync(IWebSocketContext context) => Task.CompletedTask; + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_isDisposing) + return; + + _isDisposing = true; + + if (disposing) + { + _connectionWatchdog?.Dispose(); + Task.WhenAll(_contexts.Values.Select(CloseAsync)).Await(false); + PurgeDisconnectedContexts(); + } + } + + private void RemoveWebSocket(IWebSocketContext context) + { + if (!_contexts.TryRemove(context.Id, out _)) + { + return; + } + + context.WebSocket?.Dispose(); + + // OnClientDisconnectedAsync is better called in its own task, + // so it may call methods that require a lock on _contextsAccess. + // Otherwise, calling e.g. Broadcast would result in a deadlock. +#pragma warning disable CS4014 // Call is not awaited - it is intentionally forked. + _ = Task.Run(async () => { + try + { + await OnClientDisconnectedAsync(context).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + $"[{context.Id}] OnClientDisconnectedAsync was canceled.".Debug(nameof(WebSocketModule)); + } + catch (Exception e) + { + e.Log(nameof(WebSocketModule), $"[{context.Id}] Exception in OnClientDisconnectedAsync."); + } + }); +#pragma warning restore CS4014 + } + + private void PurgeDisconnectedContexts() + { + var contexts = _contexts.Values; + var totalCount = _contexts.Count; + var purgedCount = 0; + foreach (var context in contexts) + { + if (context.WebSocket == null || context.WebSocket.State == WebSocketState.Open) + continue; + + RemoveWebSocket(context); + purgedCount++; + } + + $"{BaseRoute} - Purged {purgedCount} of {totalCount} sockets." + .Debug(nameof(WebSocketModule)); + } + + private async Task ProcessEmbedIOContext(IWebSocketContext context, CancellationToken cancellationToken) + { + ((Internal.WebSocket)context.WebSocket).OnMessage += async (s, e) => + { + if (e.Opcode == Opcode.Close) + { + await context.WebSocket.CloseAsync(context.CancellationToken).ConfigureAwait(false); + } + else + { + await OnMessageReceivedAsync( + context, + e.RawData, + new Internal.WebSocketReceiveResult(e.RawData.Length, e.Opcode)) + .ConfigureAwait(false); + } + }; + + while (context.WebSocket.State == WebSocketState.Open + || context.WebSocket.State == WebSocketState.CloseReceived + || context.WebSocket.State == WebSocketState.CloseSent) + { + await Task.Delay(500, cancellationToken).ConfigureAwait(false); + } + } + + private async Task ProcessSystemContext(IWebSocketContext context, System.Net.WebSockets.WebSocket webSocket, CancellationToken cancellationToken) + { + // define a receive buffer + var receiveBuffer = new byte[ReceiveBufferSize]; + + // define a dynamic buffer that holds multi-part receptions + var receivedMessage = new List(receiveBuffer.Length * 2); + + // poll the WebSocket connections for reception + while (webSocket.State == WebSocketState.Open) + { + // retrieve the result (blocking) + var receiveResult = new SystemWebSocketReceiveResult( + await webSocket.ReceiveAsync(new ArraySegment(receiveBuffer), cancellationToken) + .ConfigureAwait(false)); + + if (receiveResult.MessageType == (int)WebSocketMessageType.Close) + { + // close the connection if requested by the client + await webSocket + .CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken) + .ConfigureAwait(false); + return; + } + + var frameBytes = new byte[receiveResult.Count]; + Array.Copy(receiveBuffer, frameBytes, frameBytes.Length); + await OnFrameReceivedAsync(context, frameBytes, receiveResult).ConfigureAwait(false); + + // add the response to the multi-part response + receivedMessage.AddRange(frameBytes); + + if (_maxMessageSize > 0 && receivedMessage.Count > _maxMessageSize) + { + // close the connection if message exceeds max length + await webSocket.CloseAsync( + WebSocketCloseStatus.MessageTooBig, + $"Message too big. Maximum is {_maxMessageSize} bytes.", + cancellationToken).ConfigureAwait(false); + + // exit the loop; we're done + return; + } + + // if we're at the end of the message, process the message + if (!receiveResult.EndOfMessage) continue; + + await OnMessageReceivedAsync(context, receivedMessage.ToArray(), receiveResult) + .ConfigureAwait(false); + receivedMessage.Clear(); + } + } + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Collections/CollectionCacheRepository.cs b/Vendor/Swan.Lite-3.1.0/Collections/CollectionCacheRepository.cs new file mode 100644 index 0000000..bf6a54b --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Collections/CollectionCacheRepository.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Swan.Collections +{ + /// + /// A thread-safe collection cache repository for types. + /// + /// The type of member to cache. + public class CollectionCacheRepository + { + private readonly Lazy>> _data = + new Lazy>>(() => + new ConcurrentDictionary>(), true); + + /// + /// Determines whether the cache contains the specified key. + /// + /// The key. + /// true if the cache contains the key, otherwise false. + public bool ContainsKey(Type key) => _data.Value.ContainsKey(key); + + /// + /// Retrieves the properties stored for the specified type. + /// If the properties are not available, it calls the factory method to retrieve them + /// and returns them as an array of PropertyInfo. + /// + /// The key. + /// The factory. + /// + /// An array of the properties stored for the specified type. + /// + /// + /// key + /// or + /// factory. + /// + public IEnumerable Retrieve(Type key, Func> factory) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + + if (factory == null) + throw new ArgumentNullException(nameof(factory)); + + return _data.Value.GetOrAdd(key, k => factory.Invoke(k).Where(item => item != null)); + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/CompositeHashCode.cs b/Vendor/Swan.Lite-3.1.0/CompositeHashCode.cs new file mode 100644 index 0000000..b11e858 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/CompositeHashCode.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Swan +{ + /// + /// Provides a way for types that override + /// to correctly generate a hash code from the actual status of an instance. + /// CompositeHashCode must be used ONLY as a helper when implementing + /// IEquatable<T> in a STANDARD way, i.e. when: + /// + /// two instances having the same hash code are actually + /// interchangeable, i.e. they represent exactly the same object (for instance, + /// they should not coexist in a + /// SortedSet); + /// GetHashCode and + /// Equals are BOTH overridden, and the Equals + /// override either calls to the IEquatable<T>.Equals + /// (recommended) or performs exactly the same equality checks; + /// only "standard" equality checks are performed, i.e. by means of the + /// == operator, IEquatable<T> interfaces, and + /// the Equals method (for instance, this excludes case-insensitive + /// and/or culture-dependent string comparisons); + /// + /// the hash code is constructed (via Using calls) from the very same + /// fields and / or properties that are checked for equality. + /// + /// For hashing to work correctly, all fields and/or properties involved in hashing must either + /// be immutable, or at least not change while an object is referenced in a hashtable. + /// This does not refer just to System.Collections.Hashtable; the .NET + /// Framework makes a fairly extensive use of hashing, for example in + /// SortedSet<T> + /// and in various parts of LINQ. As a thumb rule, an object must stay the same during the execution of a + /// LINQ query on an IEnumerable + /// in which it is contained, as well as all the time it is referenced in a Hashtable or SortedSet. + /// + /// + /// The following code constitutes a minimal use case for CompositeHashCode, as well + /// as a reference for standard IEquatable<T> implementation. + /// Notice that all relevant properties are immutable; this is not, as stated in the summary, + /// an absolute requirement, but it surely helps and should be done every time it makes sense. + /// using System; + /// using Swan; + /// + /// namespace Example + /// { + /// public class Person : IEquatable<Person> + /// { + /// public string Name { get; private set; } + /// + /// public int Age { get; private set; } + /// + /// public Person(string name, int age) + /// { + /// Name = name; + /// Age = age; + /// } + /// + /// public override int GetHashCode() => CompositeHashCode.Using(Name, Age); + /// + /// public override bool Equals(object obj) => obj is Person other && Equals(other); + /// + /// public bool Equals(Person other) + /// => other != null + /// && other.Name == Name + /// && other.Age == Age; + /// } + /// } + /// + public static class CompositeHashCode + { + #region Private constants + + private const int InitialSeed = 17; + private const int Multiplier = 29; + + #endregion + + #region Public API + + /// + /// Computes a hash code, taking into consideration the values of the specified + /// fields and/oror properties as part of an object's state. See the + /// example. + /// + /// The values of the fields and/or properties. + /// The computed has code. + public static int Using(params object[] fields) + { + unchecked + { + return fields.Where(f => !(f is null)) + .Aggregate(InitialSeed, (current, field) => (Multiplier * current) + field.GetHashCode()); + } + } + + #endregion + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Configuration/ConfiguredObject.cs b/Vendor/Swan.Lite-3.1.0/Configuration/ConfiguredObject.cs new file mode 100644 index 0000000..1855c81 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Configuration/ConfiguredObject.cs @@ -0,0 +1,77 @@ +using System; + +namespace Swan.Configuration +{ + /// + /// Base class for objects whose configuration may be locked, + /// thus becoming read-only, at a certain moment in their lifetime. + /// + public abstract class ConfiguredObject + { + private readonly object _syncRoot = new object(); + private bool _configurationLocked; + + /// + /// Gets a value indicating whether s configuration has already been locked + /// and has therefore become read-only. + /// + /// + /// if the configuration is locked; otherwise, . + /// + /// + protected bool ConfigurationLocked + { + get + { + lock (_syncRoot) + { + return _configurationLocked; + } + } + } + + /// + /// Locks this instance's configuration, preventing further modifications. + /// + /// + /// Configuration locking must be enforced by derived classes + /// by calling at the start + /// of methods and property setters that could change the object's + /// configuration. + /// Immediately before locking the configuration, this method calls + /// as a last chance to validate configuration data, and to lock the configuration of contained objects. + /// + /// + protected void LockConfiguration() + { + lock (_syncRoot) + { + if (_configurationLocked) + return; + + OnBeforeLockConfiguration(); + _configurationLocked = true; + } + } + + /// + /// Called immediately before locking the configuration. + /// + /// + protected virtual void OnBeforeLockConfiguration() + { + } + + /// + /// Checks whether a module's configuration has become read-only + /// and, if so, throws an . + /// + /// The configuration is locked. + /// + protected void EnsureConfigurationNotLocked() + { + if (ConfigurationLocked) + throw new InvalidOperationException($"Configuration of this {GetType().Name} instance is locked."); + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Configuration/SettingsProvider.cs b/Vendor/Swan.Lite-3.1.0/Configuration/SettingsProvider.cs new file mode 100644 index 0000000..3b403db --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Configuration/SettingsProvider.cs @@ -0,0 +1,107 @@ +using Swan.Formatters; +using System; +using System.IO; + +namespace Swan.Configuration +{ + /// + /// Represents a provider to save and load settings using a plain JSON file. + /// + /// + /// The following example shows how to save and load settings. + /// + /// using Swan.Configuration; + /// + /// public class Example + /// { + /// public static void Main() + /// { + /// // get user from settings + /// var user = SettingsProvider<Settings>.Instance.Global.User; + /// + /// // modify the port + /// SettingsProvider<Settings>.Instance.Global.Port = 20; + /// + /// // if we want these settings to persist + /// SettingsProvider<Settings>.Instance.PersistGlobalSettings(); + /// } + /// + /// public class Settings + /// { + /// public int Port { get; set; } = 9696; + /// + /// public string User { get; set; } = "User"; + /// } + /// } + /// + /// + /// The type of settings model. + public sealed class SettingsProvider + : SingletonBase> + { + private readonly object _syncRoot = new object(); + + private T _global; + + /// + /// Gets or sets the configuration file path. By default the entry assembly directory is used + /// and the filename is 'appsettings.json'. + /// + /// + /// The configuration file path. + /// + public string ConfigurationFilePath { get; set; } = + Path.Combine(SwanRuntime.EntryAssemblyDirectory, "appsettings.json"); + + /// + /// Gets the global settings object. + /// + /// + /// The global settings object. + /// + public T Global + { + get + { + lock (_syncRoot) + { + if (Equals(_global, default(T))) + ReloadGlobalSettings(); + + return _global; + } + } + } + + /// + /// Reloads the global settings. + /// + public void ReloadGlobalSettings() + { + if (File.Exists(ConfigurationFilePath) == false || File.ReadAllText(ConfigurationFilePath).Length == 0) + { + ResetGlobalSettings(); + return; + } + + lock (_syncRoot) + _global = Json.Deserialize(File.ReadAllText(ConfigurationFilePath)); + } + + /// + /// Persists the global settings. + /// + public void PersistGlobalSettings() => File.WriteAllText(ConfigurationFilePath, Json.Serialize(Global, true)); + + /// + /// Resets the global settings. + /// + public void ResetGlobalSettings() + { + lock (_syncRoot) + _global = Activator.CreateInstance(); + + PersistGlobalSettings(); + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Cryptography/Hasher.cs b/Vendor/Swan.Lite-3.1.0/Cryptography/Hasher.cs new file mode 100644 index 0000000..4f49718 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Cryptography/Hasher.cs @@ -0,0 +1,124 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Swan.Cryptography +{ + /// + /// Use this class to compute a hash in MD4, SHA1, SHA256 or SHA512. + /// + public static class Hasher + { + private static readonly Lazy Md5Hasher = new Lazy(MD5.Create, true); + private static readonly Lazy SHA1Hasher = new Lazy(SHA1.Create, true); + private static readonly Lazy SHA256Hasher = new Lazy(SHA256.Create, true); + private static readonly Lazy SHA512Hasher = new Lazy(SHA512.Create, true); + + /// + /// Computes the MD5 hash of the given stream. + /// Do not use for large streams as this reads ALL bytes at once. + /// + /// The stream. + /// if set to true [create hasher]. + /// + /// The computed hash code. + /// + /// stream. + [Obsolete("Use a better hasher.")] + public static byte[] ComputeMD5(Stream @this, bool createHasher = false) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + using var md5 = MD5.Create(); + const int bufferSize = 4096; + + var readAheadBuffer = new byte[bufferSize]; + var readAheadBytesRead = @this.Read(readAheadBuffer, 0, readAheadBuffer.Length); + + do + { + var bytesRead = readAheadBytesRead; + var buffer = readAheadBuffer; + + readAheadBuffer = new byte[bufferSize]; + readAheadBytesRead = @this.Read(readAheadBuffer, 0, readAheadBuffer.Length); + + if (readAheadBytesRead == 0) + md5.TransformFinalBlock(buffer, 0, bytesRead); + else + md5.TransformBlock(buffer, 0, bytesRead, buffer, 0); + } + while (readAheadBytesRead != 0); + + return md5.Hash; + } + + /// + /// Computes the MD5 hash of the given string using UTF8 byte encoding. + /// + /// The input string. + /// if set to true [create hasher]. + /// The computed hash code. + [Obsolete("Use a better hasher.")] + public static byte[] ComputeMD5(string value, bool createHasher = false) => + ComputeMD5(Encoding.UTF8.GetBytes(value), createHasher); + + /// + /// Computes the MD5 hash of the given byte array. + /// + /// The data. + /// if set to true [create hasher]. + /// The computed hash code. + [Obsolete("Use a better hasher.")] + public static byte[] ComputeMD5(byte[] data, bool createHasher = false) => + (createHasher ? MD5.Create() : Md5Hasher.Value).ComputeHash(data); + + /// + /// Computes the SHA-1 hash of the given string using UTF8 byte encoding. + /// + /// The input string. + /// if set to true [create hasher]. + /// + /// The computes a Hash-based Message Authentication Code (HMAC) + /// using the SHA1 hash function. + /// + [Obsolete("Use a better hasher.")] + public static byte[] ComputeSha1(string @this, bool createHasher = false) + { + var inputBytes = Encoding.UTF8.GetBytes(@this); + return (createHasher ? SHA1.Create() : SHA1Hasher.Value).ComputeHash(inputBytes); + } + + /// + /// Computes the SHA-256 hash of the given string using UTF8 byte encoding. + /// + /// The input string. + /// if set to true [create hasher]. + /// + /// The computes a Hash-based Message Authentication Code (HMAC) + /// by using the SHA256 hash function. + /// + public static byte[] ComputeSha256(string value, bool createHasher = false) + { + var inputBytes = Encoding.UTF8.GetBytes(value); + return (createHasher ? SHA256.Create() : SHA256Hasher.Value).ComputeHash(inputBytes); + } + + /// + /// Computes the SHA-512 hash of the given string using UTF8 byte encoding. + /// + /// The input string. + /// if set to true [create hasher]. + /// + /// The computes a Hash-based Message Authentication Code (HMAC) + /// using the SHA512 hash function. + /// + public static byte[] ComputeSha512(string value, bool createHasher = false) + { + var inputBytes = Encoding.UTF8.GetBytes(value); + return (createHasher ? SHA512.Create() : SHA512Hasher.Value).ComputeHash(inputBytes); + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/DateTimeSpan.cs b/Vendor/Swan.Lite-3.1.0/DateTimeSpan.cs new file mode 100644 index 0000000..f512d9c --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/DateTimeSpan.cs @@ -0,0 +1,174 @@ +using System; + +namespace Swan +{ + /// + /// Represents a struct of DateTimeSpan to compare dates and get in + /// separate fields the amount of time between those dates. + /// + /// Based on https://stackoverflow.com/a/9216404/1096693. + /// + public struct DateTimeSpan + { + /// + /// Initializes a new instance of the struct. + /// + /// The years. + /// The months. + /// The days. + /// The hours. + /// The minutes. + /// The seconds. + /// The milliseconds. + public DateTimeSpan(int years, int months, int days, int hours, int minutes, int seconds, int milliseconds) + { + Years = years; + Months = months; + Days = days; + Hours = hours; + Minutes = minutes; + Seconds = seconds; + Milliseconds = milliseconds; + } + + /// + /// Gets the years. + /// + /// + /// The years. + /// + public int Years { get; } + + /// + /// Gets the months. + /// + /// + /// The months. + /// + public int Months { get; } + + /// + /// Gets the days. + /// + /// + /// The days. + /// + public int Days { get; } + + /// + /// Gets the hours. + /// + /// + /// The hours. + /// + public int Hours { get; } + + /// + /// Gets the minutes. + /// + /// + /// The minutes. + /// + public int Minutes { get; } + + /// + /// Gets the seconds. + /// + /// + /// The seconds. + /// + public int Seconds { get; } + + /// + /// Gets the milliseconds. + /// + /// + /// The milliseconds. + /// + public int Milliseconds { get; } + + internal static DateTimeSpan CompareDates(DateTime date1, DateTime date2) + { + if (date2 < date1) + { + var sub = date1; + date1 = date2; + date2 = sub; + } + + var current = date1; + var years = 0; + var months = 0; + var days = 0; + + var phase = Phase.Years; + var span = new DateTimeSpan(); + var officialDay = current.Day; + + while (phase != Phase.Done) + { + switch (phase) + { + case Phase.Years: + if (current.AddYears(years + 1) > date2) + { + phase = Phase.Months; + current = current.AddYears(years); + } + else + { + years++; + } + + break; + case Phase.Months: + if (current.AddMonths(months + 1) > date2) + { + phase = Phase.Days; + current = current.AddMonths(months); + if (current.Day < officialDay && + officialDay <= DateTime.DaysInMonth(current.Year, current.Month)) + current = current.AddDays(officialDay - current.Day); + } + else + { + months++; + } + + break; + case Phase.Days: + if (current.AddDays(days + 1) > date2) + { + current = current.AddDays(days); + var timespan = date2 - current; + span = new DateTimeSpan( + years, + months, + days, + timespan.Hours, + timespan.Minutes, + timespan.Seconds, + timespan.Milliseconds); + phase = Phase.Done; + } + else + { + days++; + } + + break; + } + } + + return span; + } + + private enum Phase + { + Years, + Months, + Days, + Done, + } + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Definitions.Types.cs b/Vendor/Swan.Lite-3.1.0/Definitions.Types.cs new file mode 100644 index 0000000..68895e9 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Definitions.Types.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net; +using Swan.Reflection; + +namespace Swan +{ + /// + /// Contains useful constants and definitions. + /// + public static partial class Definitions + { + #region Main Dictionary Definition + + /// + /// The basic types information. + /// + public static readonly Lazy> BasicTypesInfo = new Lazy>(() => + new Dictionary + { + // Non-Nullables + {typeof(DateTime), new ExtendedTypeInfo()}, + {typeof(byte), new ExtendedTypeInfo()}, + {typeof(sbyte), new ExtendedTypeInfo()}, + {typeof(int), new ExtendedTypeInfo()}, + {typeof(uint), new ExtendedTypeInfo()}, + {typeof(short), new ExtendedTypeInfo()}, + {typeof(ushort), new ExtendedTypeInfo()}, + {typeof(long), new ExtendedTypeInfo()}, + {typeof(ulong), new ExtendedTypeInfo()}, + {typeof(float), new ExtendedTypeInfo()}, + {typeof(double), new ExtendedTypeInfo()}, + {typeof(char), new ExtendedTypeInfo()}, + {typeof(bool), new ExtendedTypeInfo()}, + {typeof(decimal), new ExtendedTypeInfo()}, + {typeof(Guid), new ExtendedTypeInfo()}, + + // Strings is also considered a basic type (it's the only basic reference type) + {typeof(string), new ExtendedTypeInfo()}, + + // Nullables + {typeof(DateTime?), new ExtendedTypeInfo()}, + {typeof(byte?), new ExtendedTypeInfo()}, + {typeof(sbyte?), new ExtendedTypeInfo()}, + {typeof(int?), new ExtendedTypeInfo()}, + {typeof(uint?), new ExtendedTypeInfo()}, + {typeof(short?), new ExtendedTypeInfo()}, + {typeof(ushort?), new ExtendedTypeInfo()}, + {typeof(long?), new ExtendedTypeInfo()}, + {typeof(ulong?), new ExtendedTypeInfo()}, + {typeof(float?), new ExtendedTypeInfo()}, + {typeof(double?), new ExtendedTypeInfo()}, + {typeof(char?), new ExtendedTypeInfo()}, + {typeof(bool?), new ExtendedTypeInfo()}, + {typeof(decimal?), new ExtendedTypeInfo()}, + {typeof(Guid?), new ExtendedTypeInfo()}, + + // Additional Types + {typeof(TimeSpan), new ExtendedTypeInfo()}, + {typeof(TimeSpan?), new ExtendedTypeInfo()}, + {typeof(IPAddress), new ExtendedTypeInfo()}, + }); + + #endregion + + /// + /// Contains all basic types, including string, date time, and all of their nullable counterparts. + /// + /// + /// All basic types. + /// + public static IReadOnlyCollection AllBasicTypes { get; } = new ReadOnlyCollection(BasicTypesInfo.Value.Keys.ToArray()); + + /// + /// Gets all numeric types including their nullable counterparts. + /// Note that Booleans and Guids are not considered numeric types. + /// + /// + /// All numeric types. + /// + public static IReadOnlyCollection AllNumericTypes { get; } = new ReadOnlyCollection( + BasicTypesInfo + .Value + .Where(kvp => kvp.Value.IsNumeric) + .Select(kvp => kvp.Key).ToArray()); + + /// + /// Gets all numeric types without their nullable counterparts. + /// Note that Booleans and Guids are not considered numeric types. + /// + /// + /// All numeric value types. + /// + public static IReadOnlyCollection AllNumericValueTypes { get; } = new ReadOnlyCollection( + BasicTypesInfo + .Value + .Where(kvp => kvp.Value.IsNumeric && !kvp.Value.IsNullableValueType) + .Select(kvp => kvp.Key).ToArray()); + + /// + /// Contains all basic value types. i.e. excludes string and nullables. + /// + /// + /// All basic value types. + /// + public static IReadOnlyCollection AllBasicValueTypes { get; } = new ReadOnlyCollection( + BasicTypesInfo + .Value + .Where(kvp => kvp.Value.IsValueType) + .Select(kvp => kvp.Key).ToArray()); + + /// + /// Contains all basic value types including the string type. i.e. excludes nullables. + /// + /// + /// All basic value and string types. + /// + public static IReadOnlyCollection AllBasicValueAndStringTypes { get; } = new ReadOnlyCollection( + BasicTypesInfo + .Value + .Where(kvp => kvp.Value.IsValueType || kvp.Key == typeof(string)) + .Select(kvp => kvp.Key).ToArray()); + + /// + /// Gets all nullable value types. i.e. excludes string and all basic value types. + /// + /// + /// All basic nullable value types. + /// + public static IReadOnlyCollection AllBasicNullableValueTypes { get; } = new ReadOnlyCollection( + BasicTypesInfo + .Value + .Where(kvp => kvp.Value.IsNullableValueType) + .Select(kvp => kvp.Key).ToArray()); + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Definitions.cs b/Vendor/Swan.Lite-3.1.0/Definitions.cs new file mode 100644 index 0000000..3c4e97a --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Definitions.cs @@ -0,0 +1,39 @@ +using System.Text; + +namespace Swan +{ + /// + /// Contains useful constants and definitions. + /// + public static partial class Definitions + { + /// + /// The MS Windows codepage 1252 encoding used in some legacy scenarios + /// such as default CSV text encoding from Excel. + /// + public static readonly Encoding Windows1252Encoding; + + /// + /// The encoding associated with the default ANSI code page in the operating + /// system's regional and language settings. + /// + public static readonly Encoding CurrentAnsiEncoding; + + /// + /// Initializes the class. + /// + static Definitions() + { + CurrentAnsiEncoding = Encoding.GetEncoding(default(int)); + try + { + Windows1252Encoding = Encoding.GetEncoding(1252); + } + catch + { + // ignore, the codepage is not available use default + Windows1252Encoding = CurrentAnsiEncoding; + } + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Diagnostics/Benchmark.cs b/Vendor/Swan.Lite-3.1.0/Diagnostics/Benchmark.cs new file mode 100644 index 0000000..d79f98b --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Diagnostics/Benchmark.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; + +namespace Swan.Diagnostics +{ + /// + /// A simple benchmarking class. + /// + /// + /// The following code demonstrates how to create a simple benchmark. + /// + /// namespace Examples.Benchmark.Simple + /// { + /// using Swan.Diagnostics; + /// + /// public class SimpleBenchmark + /// { + /// public static void Main() + /// { + /// using (Benchmark.Start("Test")) + /// { + /// // do some logic in here + /// } + /// + /// // dump results into a string + /// var results = Benchmark.Dump(); + /// } + /// } + /// + /// } + /// + /// + public static partial class Benchmark + { + private static readonly object SyncLock = new object(); + private static readonly Dictionary> Measures = new Dictionary>(); + + /// + /// Starts measuring with the given identifier. + /// + /// The identifier. + /// A disposable object that when disposed, adds a benchmark result. + public static IDisposable Start(string identifier) => new BenchmarkUnit(identifier); + + /// + /// Outputs the benchmark statistics. + /// + /// A string containing human-readable statistics. + public static string Dump() + { + var builder = new StringBuilder(); + + lock (SyncLock) + { + foreach (var kvp in Measures) + { + builder.Append($"BID: {kvp.Key,-30} | ") + .Append($"CNT: {kvp.Value.Count,6} | ") + .Append($"AVG: {kvp.Value.Average(t => t.TotalMilliseconds),8:0.000} ms. | ") + .Append($"MAX: {kvp.Value.Max(t => t.TotalMilliseconds),8:0.000} ms. | ") + .Append($"MIN: {kvp.Value.Min(t => t.TotalMilliseconds),8:0.000} ms. | ") + .Append(Environment.NewLine); + } + } + + return builder.ToString().TrimEnd(); + } + + /// + /// Measures the elapsed time of the given action as a TimeSpan + /// This method uses a high precision Stopwatch if it is available. + /// + /// The target. + /// + /// A time interval that represents a specified time, where the specification is in units of ticks. + /// + /// target. + public static TimeSpan BenchmarkAction(Action target) + { + if (target == null) + throw new ArgumentNullException(nameof(target)); + + var sw = Stopwatch.IsHighResolution ? new HighResolutionTimer() : new Stopwatch(); + + try + { + sw.Start(); + target.Invoke(); + } + catch + { + // swallow + } + finally + { + sw.Stop(); + } + + return TimeSpan.FromTicks(sw.ElapsedTicks); + } + + /// + /// Adds the specified result to the given identifier. + /// + /// The identifier. + /// The elapsed. + private static void Add(string identifier, TimeSpan elapsed) + { + lock (SyncLock) + { + if (Measures.ContainsKey(identifier) == false) + Measures[identifier] = new List(1024 * 1024); + + Measures[identifier].Add(elapsed); + } + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Diagnostics/BenchmarkUnit.cs b/Vendor/Swan.Lite-3.1.0/Diagnostics/BenchmarkUnit.cs new file mode 100644 index 0000000..e896995 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Diagnostics/BenchmarkUnit.cs @@ -0,0 +1,50 @@ +using System; +using System.Diagnostics; + +namespace Swan.Diagnostics +{ + public static partial class Benchmark + { + /// + /// Represents a disposable benchmark unit. + /// + /// + private sealed class BenchmarkUnit : IDisposable + { + private readonly string _identifier; + private bool _isDisposed; // To detect redundant calls + private Stopwatch? _stopwatch = new Stopwatch(); + + /// + /// Initializes a new instance of the class. + /// + /// The identifier. + public BenchmarkUnit(string identifier) + { + _identifier = identifier; + _stopwatch?.Start(); + } + + /// + public void Dispose() => Dispose(true); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + private void Dispose(bool alsoManaged) + { + if (_isDisposed) return; + + if (alsoManaged) + { + Add(_identifier, _stopwatch?.Elapsed ?? default); + _stopwatch?.Stop(); + } + + _stopwatch = null; + _isDisposed = true; + } + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Diagnostics/HighResolutionTimer.cs b/Vendor/Swan.Lite-3.1.0/Diagnostics/HighResolutionTimer.cs new file mode 100644 index 0000000..b419d09 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Diagnostics/HighResolutionTimer.cs @@ -0,0 +1,32 @@ +namespace Swan.Diagnostics +{ + using System; + using System.Diagnostics; + + /// + /// Provides access to a high-resolution, time measuring device. + /// + /// + public class HighResolutionTimer : Stopwatch + { + /// + /// Initializes a new instance of the class. + /// + /// High-resolution timer not available. + public HighResolutionTimer() + { + if (!IsHighResolution) + throw new NotSupportedException("High-resolution timer not available"); + } + + /// + /// Gets the number of microseconds per timer tick. + /// + public static double MicrosecondsPerTick { get; } = 1000000d / Frequency; + + /// + /// Gets the elapsed microseconds. + /// + public long ElapsedMicroseconds => (long)(ElapsedTicks * MicrosecondsPerTick); + } +} diff --git a/Vendor/Swan.Lite-3.1.0/EnumHelper.cs b/Vendor/Swan.Lite-3.1.0/EnumHelper.cs new file mode 100644 index 0000000..cf49ec0 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/EnumHelper.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Swan.Collections; + +namespace Swan +{ + /// + /// Provide Enumerations helpers with internal cache. + /// + public class EnumHelper + : SingletonBase>> + { + /// + /// Gets all the names and enumerators from a specific Enum type. + /// + /// The type of the attribute to be retrieved. + /// A tuple of enumerator names and their value stored for the specified type. + public static IEnumerable> Retrieve() + where T : struct, IConvertible + { + return Instance.Retrieve(typeof(T), t => Enum.GetValues(t) + .Cast() + .Select(item => Tuple.Create(Enum.GetName(t, item), item))); + } + + /// + /// Gets the cached items with the enum item value. + /// + /// The type of enumeration. + /// if set to true [humanize]. + /// + /// A collection of Type/Tuple pairs + /// that represents items with the enum item value. + /// + public static IEnumerable> GetItemsWithValue(bool humanize = true) + where T : struct, IConvertible + { + return Retrieve() + .Select(x => Tuple.Create((int) x.Item2, humanize ? x.Item1.Humanize() : x.Item1)); + } + + /// + /// Gets the flag values. + /// + /// The type of the enum. + /// The value. + /// if set to true [ignore zero]. + /// + /// A list of values in the flag. + /// + public static IEnumerable GetFlagValues(int value, bool ignoreZero = false) + where TEnum : struct, IConvertible + { + return Retrieve() + .Select(x => (int) x.Item2) + .When(() => ignoreZero, q => q.Where(f => f != 0)) + .Where(x => (x & value) == x); + } + + /// + /// Gets the flag values. + /// + /// The type of the enum. + /// The value. + /// if set to true [ignore zero]. + /// + /// A list of values in the flag. + /// + public static IEnumerable GetFlagValues(long value, bool ignoreZero = false) + where TEnum : struct, IConvertible + { + return Retrieve() + .Select(x => (long) x.Item2) + .When(() => ignoreZero, q => q.Where(f => f != 0)) + .Where(x => (x & value) == x); + } + + /// + /// Gets the flag values. + /// + /// The type of the enum. + /// The value. + /// if set to true [ignore zero]. + /// + /// A list of values in the flag. + /// + public static IEnumerable GetFlagValues(byte value, bool ignoreZero = false) + where TEnum : struct, IConvertible + { + return Retrieve() + .Select(x => (byte) x.Item2) + .When(() => ignoreZero, q => q.Where(f => f != 0)) + .Where(x => (x & value) == x); + } + + /// + /// Gets the flag names. + /// + /// The type of the enum. + /// the value. + /// if set to true [ignore zero]. + /// if set to true [humanize]. + /// + /// A list of flag names. + /// + public static IEnumerable GetFlagNames(int value, bool ignoreZero = false, bool humanize = true) + where TEnum : struct, IConvertible + { + return Retrieve() + .When(() => ignoreZero, q => q.Where(f => (int) f.Item2 != 0)) + .Where(x => ((int) x.Item2 & value) == (int) x.Item2) + .Select(x => humanize ? x.Item1.Humanize() : x.Item1); + } + + /// + /// Gets the flag names. + /// + /// The type of the enum. + /// The value. + /// if set to true [ignore zero]. + /// if set to true [humanize]. + /// + /// A list of flag names. + /// + public static IEnumerable GetFlagNames(long value, bool ignoreZero = false, bool humanize = true) + where TEnum : struct, IConvertible + { + return Retrieve() + .When(() => ignoreZero, q => q.Where(f => (long) f.Item2 != 0)) + .Where(x => ((long) x.Item2 & value) == (long) x.Item2) + .Select(x => humanize ? x.Item1.Humanize() : x.Item1); + } + + /// + /// Gets the flag names. + /// + /// The type of the enum. + /// The value. + /// if set to true [ignore zero]. + /// if set to true [humanize]. + /// + /// A list of flag names. + /// + public static IEnumerable GetFlagNames(byte value, bool ignoreZero = false, bool humanize = true) + where TEnum : struct, IConvertible + { + return Retrieve() + .When(() => ignoreZero, q => q.Where(f => (byte) f.Item2 != 0)) + .Where(x => ((byte) x.Item2 & value) == (byte) x.Item2) + .Select(x => humanize ? x.Item1.Humanize() : x.Item1); + } + + /// + /// Gets the cached items with the enum item index. + /// + /// The type of enumeration. + /// if set to true [humanize]. + /// + /// A collection of Type/Tuple pairs that represents items with the enum item value. + /// + public static IEnumerable> GetItemsWithIndex(bool humanize = true) + where T : struct, IConvertible + { + var i = 0; + + return Retrieve() + .Select(x => Tuple.Create(i++, humanize ? x.Item1.Humanize() : x.Item1)); + } + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Enums.cs b/Vendor/Swan.Lite-3.1.0/Enums.cs new file mode 100644 index 0000000..f3dc604 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Enums.cs @@ -0,0 +1,44 @@ +namespace Swan +{ + /// + /// Enumeration of Operating Systems. + /// + public enum OperatingSystem + { + /// + /// Unknown OS + /// + Unknown, + + /// + /// Windows + /// + Windows, + + /// + /// UNIX/Linux + /// + Unix, + + /// + /// macOS (OSX) + /// + Osx, + } + + /// + /// Defines Endianness, big or little. + /// + public enum Endianness + { + /// + /// In big endian, you store the most significant byte in the smallest address. + /// + Big, + + /// + /// In little endian, you store the least significant byte in the smallest address. + /// + Little, + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Extensions.ByteArrays.cs b/Vendor/Swan.Lite-3.1.0/Extensions.ByteArrays.cs new file mode 100644 index 0000000..bd025ce --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Extensions.ByteArrays.cs @@ -0,0 +1,511 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Swan +{ + /// + /// Provides various extension methods for byte arrays and streams. + /// + public static class ByteArrayExtensions + { + /// + /// Converts an array of bytes to its lower-case, hexadecimal representation. + /// + /// The bytes. + /// if set to true add the 0x prefix tot he output. + /// + /// The specified string instance; no actual conversion is performed. + /// + /// bytes. + public static string ToLowerHex(this byte[] bytes, bool addPrefix = false) + => ToHex(bytes, addPrefix, "x2"); + + /// + /// Converts an array of bytes to its upper-case, hexadecimal representation. + /// + /// The bytes. + /// if set to true [add prefix]. + /// + /// The specified string instance; no actual conversion is performed. + /// + /// bytes. + public static string ToUpperHex(this byte[] bytes, bool addPrefix = false) + => ToHex(bytes, addPrefix, "X2"); + + /// + /// Converts an array of bytes to a sequence of dash-separated, hexadecimal, + /// uppercase characters. + /// + /// The bytes. + /// + /// A string of hexadecimal pairs separated by hyphens, where each pair represents + /// the corresponding element in value; for example, "7F-2C-4A-00". + /// + public static string ToDashedHex(this byte[] bytes) => BitConverter.ToString(bytes); + + /// + /// Converts an array of bytes to a base-64 encoded string. + /// + /// The bytes. + /// A converted from an array of bytes. + public static string ToBase64(this byte[] bytes) => Convert.ToBase64String(bytes); + + /// + /// Converts a set of hexadecimal characters (uppercase or lowercase) + /// to a byte array. String length must be a multiple of 2 and + /// any prefix (such as 0x) has to be avoided for this to work properly. + /// + /// The hexadecimal. + /// + /// A byte array containing the results of encoding the specified set of characters. + /// + /// hex. + public static byte[] ConvertHexadecimalToBytes(this string @this) + { + if (string.IsNullOrWhiteSpace(@this)) + throw new ArgumentNullException(nameof(@this)); + + return Enumerable + .Range(0, @this.Length / 2) + .Select(x => Convert.ToByte(@this.Substring(x * 2, 2), 16)) + .ToArray(); + } + + /// + /// Gets the bit value at the given offset. + /// + /// The b. + /// The offset. + /// The length. + /// + /// Bit value at the given offset. + /// + public static byte GetBitValueAt(this byte @this, byte offset, byte length = 1) => (byte)((@this >> offset) & ~(0xff << length)); + + /// + /// Sets the bit value at the given offset. + /// + /// The b. + /// The offset. + /// The length. + /// The value. + /// Bit value at the given offset. + public static byte SetBitValueAt(this byte @this, byte offset, byte length, byte value) + { + var mask = ~(0xff << length); + var valueAt = (byte)(value & mask); + + return (byte)((valueAt << offset) | (@this & ~(mask << offset))); + } + + /// + /// Sets the bit value at the given offset. + /// + /// The b. + /// The offset. + /// The value. + /// Bit value at the given offset. + public static byte SetBitValueAt(this byte @this, byte offset, byte value) => @this.SetBitValueAt(offset, 1, value); + + /// + /// Splits a byte array delimited by the specified sequence of bytes. + /// Each individual element in the result will contain the split sequence terminator if it is found to be delimited by it. + /// For example if you split [1,2,3,4] by a sequence of [2,3] this method will return a list with 2 byte arrays, one containing [1,2,3] and the + /// second one containing 4. Use the Trim extension methods to remove terminator sequences. + /// + /// The buffer. + /// The offset at which to start splitting bytes. Any bytes before this will be discarded. + /// The sequence. + /// + /// A byte array containing the results the specified sequence of bytes. + /// + /// + /// buffer + /// or + /// sequence. + /// + public static List Split(this byte[] @this, int offset, params byte[] sequence) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + if (sequence == null) + throw new ArgumentNullException(nameof(sequence)); + + var seqOffset = offset.Clamp(0, @this.Length - 1); + + var result = new List(); + + while (seqOffset < @this.Length) + { + var separatorStartIndex = @this.GetIndexOf(sequence, seqOffset); + + if (separatorStartIndex >= 0) + { + var item = new byte[separatorStartIndex - seqOffset + sequence.Length]; + Array.Copy(@this, seqOffset, item, 0, item.Length); + result.Add(item); + seqOffset += item.Length; + } + else + { + var item = new byte[@this.Length - seqOffset]; + Array.Copy(@this, seqOffset, item, 0, item.Length); + result.Add(item); + break; + } + } + + return result; + } + + /// + /// Clones the specified buffer, byte by byte. + /// + /// The buffer. + /// + /// A byte array containing the results of encoding the specified set of characters. + /// + /// this + public static byte[] DeepClone(this byte[] @this) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + var result = new byte[@this.Length]; + Array.Copy(@this, result, @this.Length); + return result; + } + + /// + /// Removes the specified sequence from the start of the buffer if the buffer begins with such sequence. + /// + /// The buffer. + /// The sequence. + /// + /// A new trimmed byte array. + /// + /// buffer. + public static byte[] TrimStart(this byte[] buffer, params byte[] sequence) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + if (buffer.StartsWith(sequence) == false) + return buffer.DeepClone(); + + var result = new byte[buffer.Length - sequence.Length]; + Array.Copy(buffer, sequence.Length, result, 0, result.Length); + return result; + } + + /// + /// Removes the specified sequence from the end of the buffer if the buffer ends with such sequence. + /// + /// The buffer. + /// The sequence. + /// + /// A byte array containing the results of encoding the specified set of characters. + /// + /// buffer. + public static byte[] TrimEnd(this byte[] buffer, params byte[] sequence) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + if (buffer.EndsWith(sequence) == false) + return buffer.DeepClone(); + + var result = new byte[buffer.Length - sequence.Length]; + Array.Copy(buffer, 0, result, 0, result.Length); + return result; + } + + /// + /// Removes the specified sequence from the end and the start of the buffer + /// if the buffer ends and/or starts with such sequence. + /// + /// The buffer. + /// The sequence. + /// A byte array containing the results of encoding the specified set of characters. + public static byte[] Trim(this byte[] buffer, params byte[] sequence) + { + var trimStart = buffer.TrimStart(sequence); + return trimStart.TrimEnd(sequence); + } + + /// + /// Determines if the specified buffer ends with the given sequence of bytes. + /// + /// The buffer. + /// The sequence. + /// + /// True if the specified buffer is ends; otherwise, false. + /// + /// buffer. + public static bool EndsWith(this byte[] buffer, params byte[] sequence) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + var startIndex = buffer.Length - sequence.Length; + return buffer.GetIndexOf(sequence, startIndex) == startIndex; + } + + /// + /// Determines if the specified buffer starts with the given sequence of bytes. + /// + /// The buffer. + /// The sequence. + /// true if the specified buffer starts; otherwise, false. + public static bool StartsWith(this byte[] buffer, params byte[] sequence) => buffer.GetIndexOf(sequence) == 0; + + /// + /// Determines whether the buffer contains the specified sequence. + /// + /// The buffer. + /// The sequence. + /// + /// true if [contains] [the specified sequence]; otherwise, false. + /// + public static bool Contains(this byte[] buffer, params byte[] sequence) => buffer.GetIndexOf(sequence) >= 0; + + /// + /// Determines whether the buffer exactly matches, byte by byte the specified sequence. + /// + /// The buffer. + /// The sequence. + /// + /// true if [is equal to] [the specified sequence]; otherwise, false. + /// + /// buffer. + public static bool IsEqualTo(this byte[] buffer, params byte[] sequence) + { + if (ReferenceEquals(buffer, sequence)) + return true; + + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + return buffer.Length == sequence.Length && buffer.GetIndexOf(sequence) == 0; + } + + /// + /// Returns the first instance of the matched sequence based on the given offset. + /// If no matches are found then this method returns -1. + /// + /// The buffer. + /// The sequence. + /// The offset. + /// The index of the sequence. + /// + /// buffer + /// or + /// sequence. + /// + public static int GetIndexOf(this byte[] buffer, byte[] sequence, int offset = 0) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + if (sequence == null) + throw new ArgumentNullException(nameof(sequence)); + if (sequence.Length == 0) + return -1; + if (sequence.Length > buffer.Length) + return -1; + + var seqOffset = offset < 0 ? 0 : offset; + + var matchedCount = 0; + for (var i = seqOffset; i < buffer.Length; i++) + { + if (buffer[i] == sequence[matchedCount]) + matchedCount++; + else + matchedCount = 0; + + if (matchedCount == sequence.Length) + return i - (matchedCount - 1); + } + + return -1; + } + + /// + /// Appends the Memory Stream with the specified buffer. + /// + /// The stream. + /// The buffer. + /// + /// The same MemoryStream instance. + /// + /// + /// stream + /// or + /// buffer. + /// + public static MemoryStream Append(this MemoryStream stream, byte[] buffer) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + stream.Write(buffer, 0, buffer.Length); + return stream; + } + + /// + /// Appends the Memory Stream with the specified buffer. + /// + /// The stream. + /// The buffer. + /// + /// Block of bytes to the current stream using data read from a buffer. + /// + /// buffer. + public static MemoryStream Append(this MemoryStream stream, IEnumerable buffer) => Append(stream, buffer?.ToArray()); + + /// + /// Appends the Memory Stream with the specified set of buffers. + /// + /// The stream. + /// The buffers. + /// + /// Block of bytes to the current stream using data read from a buffer. + /// + /// buffers. + public static MemoryStream Append(this MemoryStream stream, IEnumerable buffers) + { + if (buffers == null) + throw new ArgumentNullException(nameof(buffers)); + + foreach (var buffer in buffers) + Append(stream, buffer); + + return stream; + } + + /// + /// Converts an array of bytes into text with the specified encoding. + /// + /// The buffer. + /// The encoding. + /// A that contains the results of decoding the specified sequence of bytes. + public static string ToText(this IEnumerable buffer, Encoding encoding) => + encoding == null + ? throw new ArgumentNullException(nameof(encoding)) + : encoding.GetString(buffer.ToArray()); + + /// + /// Converts an array of bytes into text with UTF8 encoding. + /// + /// The buffer. + /// A that contains the results of decoding the specified sequence of bytes. + public static string ToText(this IEnumerable buffer) => buffer.ToText(Encoding.UTF8); + + /// + /// Reads the bytes asynchronous. + /// + /// The stream. + /// The length. + /// Length of the buffer. + /// The cancellation token. + /// + /// A byte array containing the results of encoding the specified set of characters. + /// + /// stream. + public static async Task ReadBytesAsync(this Stream stream, long length, int bufferLength, CancellationToken cancellationToken = default) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + using var dest = new MemoryStream(); + + try + { + var buff = new byte[bufferLength]; + while (length > 0) + { + if (length < bufferLength) + bufferLength = (int)length; + + var read = await stream.ReadAsync(buff, 0, bufferLength, cancellationToken).ConfigureAwait(false); + if (read == 0) + break; + + dest.Write(buff, 0, read); + length -= read; + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // ignored + } + + return dest.ToArray(); + } + + /// + /// Reads the bytes asynchronous. + /// + /// The stream. + /// The length. + /// The cancellation token. + /// + /// A byte array containing the results of encoding the specified set of characters. + /// + /// stream. + public static async Task ReadBytesAsync(this Stream stream, int length, CancellationToken cancellationToken = default) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + var buff = new byte[length]; + var offset = 0; + + try + { + while (length > 0) + { + var read = await stream.ReadAsync(buff, offset, length, cancellationToken).ConfigureAwait(false); + if (read == 0) + break; + + offset += read; + length -= read; + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // ignored + } + + return new ArraySegment(buff, 0, offset).ToArray(); + } + + private static string ToHex(byte[] bytes, bool addPrefix, string format) + { + if (bytes == null) + throw new ArgumentNullException(nameof(bytes)); + + var sb = new StringBuilder(bytes.Length * 2); + + foreach (var item in bytes) + sb.Append(item.ToString(format, CultureInfo.InvariantCulture)); + + return $"{(addPrefix ? "0x" : string.Empty)}{sb}"; + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Extensions.Dates.cs b/Vendor/Swan.Lite-3.1.0/Extensions.Dates.cs new file mode 100644 index 0000000..811df20 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Extensions.Dates.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Swan +{ + /// + /// Provides extension methods for . + /// + public static class DateExtensions + { + private static readonly Dictionary DateRanges = new Dictionary() + { + { "minute", 59}, + { "hour", 23}, + { "dayOfMonth", 31}, + { "month", 12}, + { "dayOfWeek", 6}, + }; + + /// + /// Converts the date to a YYYY-MM-DD string. + /// + /// The on which this method is called. + /// The concatenation of date.Year, date.Month and date.Day. + public static string ToSortableDate(this DateTime @this) + => $"{@this.Year:0000}-{@this.Month:00}-{@this.Day:00}"; + + /// + /// Converts the date to a YYYY-MM-DD HH:II:SS string. + /// + /// The on which this method is called. + /// The concatenation of date.Year, date.Month, date.Day, date.Hour, date.Minute and date.Second. + public static string ToSortableDateTime(this DateTime @this) + => $"{@this.Year:0000}-{@this.Month:00}-{@this.Day:00} {@this.Hour:00}:{@this.Minute:00}:{@this.Second:00}"; + + /// + /// Parses a YYYY-MM-DD and optionally it time part, HH:II:SS into a DateTime. + /// + /// The sortable date. + /// + /// A new instance of the DateTime structure to + /// the specified year, month, day, hour, minute and second. + /// + /// sortableDate. + /// + /// Represents errors that occur during application execution. + /// + /// + /// Unable to parse sortable date and time. - sortableDate. + /// + public static DateTime ToDateTime(this string @this) + { + if (string.IsNullOrWhiteSpace(@this)) + throw new ArgumentNullException(nameof(@this)); + + var hour = 0; + var minute = 0; + var second = 0; + + var dateTimeParts = @this.Split(' '); + + try + { + if (dateTimeParts.Length != 1 && dateTimeParts.Length != 2) + throw new Exception(); + + var dateParts = dateTimeParts[0].Split('-'); + if (dateParts.Length != 3) throw new Exception(); + + var year = int.Parse(dateParts[0]); + var month = int.Parse(dateParts[1]); + var day = int.Parse(dateParts[2]); + + if (dateTimeParts.Length > 1) + { + var timeParts = dateTimeParts[1].Split(':'); + if (timeParts.Length != 3) throw new Exception(); + + hour = int.Parse(timeParts[0]); + minute = int.Parse(timeParts[1]); + second = int.Parse(timeParts[2]); + } + + return new DateTime(year, month, day, hour, minute, second); + } + catch (Exception) + { + throw new ArgumentException("Unable to parse sortable date and time.", nameof(@this)); + } + } + + /// + /// Creates a date range. + /// + /// The start date. + /// The end date. + /// + /// A sequence of integral numbers within a specified date's range. + /// + public static IEnumerable DateRange(this DateTime startDate, DateTime endDate) + => Enumerable.Range(0, (endDate - startDate).Days + 1).Select(d => startDate.AddDays(d)); + + /// + /// Rounds up a date to match a timespan. + /// + /// The datetime. + /// The timespan to match. + /// + /// A new instance of the DateTime structure to the specified datetime and timespan ticks. + /// + public static DateTime RoundUp(this DateTime date, TimeSpan timeSpan) + => new DateTime(((date.Ticks + timeSpan.Ticks - 1) / timeSpan.Ticks) * timeSpan.Ticks); + + /// + /// Get this datetime as a Unix epoch timestamp (seconds since Jan 1, 1970, midnight UTC). + /// + /// The on which this method is called. + /// Seconds since Unix epoch. + public static long ToUnixEpochDate(this DateTime @this) => new DateTimeOffset(@this).ToUniversalTime().ToUnixTimeSeconds(); + + /// + /// Compares a Date to another and returns a DateTimeSpan. + /// + /// The date start. + /// The date end. + /// A DateTimeSpan with the Years, Months, Days, Hours, Minutes, Seconds and Milliseconds between the dates. + public static DateTimeSpan GetDateTimeSpan(this DateTime dateStart, DateTime dateEnd) + => DateTimeSpan.CompareDates(dateStart, dateEnd); + + /// + /// Compare the Date elements(Months, Days, Hours, Minutes). + /// + /// The on which this method is called. + /// The minute (0-59). + /// The hour. (0-23). + /// The day of month. (1-31). + /// The month. (1-12). + /// The day of week. (0-6)(Sunday = 0). + /// Returns true if Months, Days, Hours and Minutes match, otherwise false. + public static bool AsCronCanRun(this DateTime @this, int? minute = null, int? hour = null, int? dayOfMonth = null, int? month = null, int? dayOfWeek = null) + { + var results = new List + { + GetElementParts(minute, @this.Minute), + GetElementParts(hour, @this.Hour), + GetElementParts(dayOfMonth, @this.Day), + GetElementParts(month, @this.Month), + GetElementParts(dayOfWeek, (int) @this.DayOfWeek), + }; + + return results.Any(x => x != false); + } + + /// + /// Compare the Date elements(Months, Days, Hours, Minutes). + /// + /// The on which this method is called. + /// The minute (0-59). + /// The hour. (0-23). + /// The day of month. (1-31). + /// The month. (1-12). + /// The day of week. (0-6)(Sunday = 0). + /// Returns true if Months, Days, Hours and Minutes match, otherwise false. + public static bool AsCronCanRun(this DateTime @this, string minute = "*", string hour = "*", string dayOfMonth = "*", string month = "*", string dayOfWeek = "*") + { + var results = new List + { + GetElementParts(minute, nameof(minute), @this.Minute), + GetElementParts(hour, nameof(hour), @this.Hour), + GetElementParts(dayOfMonth, nameof(dayOfMonth), @this.Day), + GetElementParts(month, nameof(month), @this.Month), + GetElementParts(dayOfWeek, nameof(dayOfWeek), (int) @this.DayOfWeek), + }; + + return results.Any(x => x != false); + } + + /// + /// Converts a to the RFC1123 format. + /// + /// The on which this method is called. + /// The string representation of according to RFC1123. + /// + /// If is not a UTC date / time, its UTC equivalent is converted, leaving unchanged. + /// + public static string ToRfc1123String(this DateTime @this) + => @this.ToUniversalTime().ToString("R", CultureInfo.InvariantCulture); + + private static bool? GetElementParts(int? status, int value) => status.HasValue ? status.Value == value : (bool?) null; + + private static bool? GetElementParts(string parts, string type, int value) + { + if (string.IsNullOrWhiteSpace(parts) || parts == "*") + return null; + + if (parts.Contains(",")) + { + return parts.Split(',').Select(int.Parse).Contains(value); + } + + var stop = DateRanges[type]; + + if (parts.Contains("/")) + { + var multiple = int.Parse(parts.Split('/').Last()); + var start = type == "dayOfMonth" || type == "month" ? 1 : 0; + + for (var i = start; i <= stop; i += multiple) + if (i == value) return true; + + return false; + } + + if (parts.Contains("-")) + { + var range = parts.Split('-'); + var start = int.Parse(range.First()); + stop = Math.Max(stop, int.Parse(range.Last())); + + if ((type == "dayOfMonth" || type == "month") && start == 0) + start = 1; + + for (var i = start; i <= stop; i++) + if (i == value) return true; + + return false; + } + + return int.Parse(parts) == value; + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Extensions.Dictionaries.cs b/Vendor/Swan.Lite-3.1.0/Extensions.Dictionaries.cs new file mode 100644 index 0000000..bb830cf --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Extensions.Dictionaries.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; + +namespace Swan +{ + /// + /// Extension methods. + /// + public static partial class Extensions + { + /// + /// Gets the value if exists or default. + /// + /// The type of the key. + /// The type of the value. + /// The dictionary. + /// The key. + /// The default value. + /// + /// The value of the provided key or default. + /// + /// dict. + public static TValue GetValueOrDefault(this IDictionary dict, TKey key, TValue defaultValue = default) + { + if (dict == null) + throw new ArgumentNullException(nameof(dict)); + + return dict.ContainsKey(key) ? dict[key] : defaultValue; + } + + /// + /// Adds a key/value pair to the Dictionary if the key does not already exist. + /// If the value is null, the key will not be updated. + /// Based on ConcurrentDictionary.GetOrAdd method. + /// + /// The type of the key. + /// The type of the value. + /// The dictionary. + /// The key. + /// The value factory. + /// + /// The value for the key. + /// + /// + /// dict + /// or + /// valueFactory. + /// + public static TValue GetOrAdd(this IDictionary dict, TKey key, Func valueFactory) + { + if (dict == null) + throw new ArgumentNullException(nameof(dict)); + + if (valueFactory == null) + throw new ArgumentNullException(nameof(valueFactory)); + + if (!dict.ContainsKey(key)) + { + var value = valueFactory(key); + if (Equals(value, default)) return default; + dict[key] = value; + } + + return dict[key]; + } + + /// + /// Executes the item action for each element in the Dictionary. + /// + /// The type of the key. + /// The type of the value. + /// The dictionary. + /// The item action. + /// dict. + public static void ForEach(this IDictionary dict, Action itemAction) + { + if (dict == null) + throw new ArgumentNullException(nameof(dict)); + + foreach (var kvp in dict) + { + itemAction(kvp.Key, kvp.Value); + } + } + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Extensions.Enumerable.cs b/Vendor/Swan.Lite-3.1.0/Extensions.Enumerable.cs new file mode 100644 index 0000000..e7a60b7 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Extensions.Enumerable.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Swan +{ + /// + /// This class contains extension methods for types implementing IEnumerable<TSource> + /// + public static class EnumerableExtensions + { + /// + /// This method returns the Union + /// of all non-null parameters. + /// + /// The type of the elements of the input sequences. + /// An IEnumerable<TSource> whose distinct elements forms the first set of the union. + /// An IEnumerable<TSource> whose distinct elements forms the second set of the union. + /// + /// An that contains the elements from non-null input sequences, excluding duplicates. + /// + public static IEnumerable UnionExcludingNulls(this IEnumerable @this, IEnumerable second) + => Enumerable.Union( + @this ?? Enumerable.Empty(), + second ?? Enumerable.Empty()); + + /// + /// This method returns the Union + /// of all non-null parameters. + /// + /// The type of the elements of the input sequences. + /// An IEnumerable<TSource> whose distinct elements forms the first set of the union. + /// An IEnumerable<TSource> whose distinct elements forms the second set of the union. + /// The IEqualityComparer<TSource> to compare values. + /// + /// An that contains the elements from non-null input sequences, excluding duplicates. + /// + public static IEnumerable UnionExcludingNulls(this IEnumerable @this, IEnumerable second, IEqualityComparer comparer) + => Enumerable.Union( + @this ?? Enumerable.Empty(), + second ?? Enumerable.Empty(), + comparer); + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Extensions.Exceptions.cs b/Vendor/Swan.Lite-3.1.0/Extensions.Exceptions.cs new file mode 100644 index 0000000..02b7d06 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Extensions.Exceptions.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using System.Runtime.ExceptionServices; +using System.Threading; + +namespace Swan +{ + /// + /// Provides extension methods for . + /// + public static class ExceptionExtensions + { + /// + /// Returns a value that tells whether an is of a type that + /// we better not catch and ignore. + /// + /// The exception being thrown. + /// if is a critical exception; + /// otherwise, . + public static bool IsCriticalException(this Exception @this) + => @this.IsCriticalExceptionCore() + || (@this.InnerException?.IsCriticalException() ?? false) + || (@this is AggregateException aggregateException && aggregateException.InnerExceptions.Any(e => e.IsCriticalException())); + + /// + /// Returns a value that tells whether an is of a type that + /// will likely cause application failure. + /// + /// The exception being thrown. + /// if is a fatal exception; + /// otherwise, . + public static bool IsFatalException(this Exception @this) + => @this.IsFatalExceptionCore() + || (@this.InnerException?.IsFatalException() ?? false) + || (@this is AggregateException aggregateException && aggregateException.InnerExceptions.Any(e => e.IsFatalException())); + + /// + /// Rethrows an already-thrown exception, preserving the stack trace of the original throw. + /// This method does not return; its return type is an exception type so it can be used + /// with throw semantics, e.g.: throw ex.RethrowPreservingStackTrace();, + /// to let static code analysis tools that it throws instead of returning. + /// + /// The exception to rethrow. + /// This method should never return; if it does, it is an indication of an internal error, + /// so it returns an instance of . + public static InternalErrorException RethrowPreservingStackTrace(this Exception @this) + { + ExceptionDispatchInfo.Capture(@this).Throw(); + return SelfCheck.Failure("Reached unreachable code."); + } + + private static bool IsCriticalExceptionCore(this Exception @this) + => IsFatalExceptionCore(@this) + || @this is AppDomainUnloadedException + || @this is BadImageFormatException + || @this is CannotUnloadAppDomainException + || @this is InvalidProgramException + || @this is NullReferenceException; + + private static bool IsFatalExceptionCore(this Exception @this) + => @this is StackOverflowException + || @this is OutOfMemoryException + || @this is ThreadAbortException + || @this is AccessViolationException; + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Extensions.Functional.cs b/Vendor/Swan.Lite-3.1.0/Extensions.Functional.cs new file mode 100644 index 0000000..dcbcbf9 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Extensions.Functional.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Swan +{ + /// + /// Functional programming extension methods. + /// + public static class FunctionalExtensions + { + /// + /// Whens the specified condition. + /// + /// The type of IQueryable. + /// The list. + /// The condition. + /// The function. + /// + /// The IQueryable. + /// + /// + /// this + /// or + /// condition + /// or + /// fn. + /// + public static IQueryable When( + this IQueryable list, + Func condition, + Func, IQueryable> fn) + { + if (list == null) + throw new ArgumentNullException(nameof(list)); + + if (condition == null) + throw new ArgumentNullException(nameof(condition)); + + if (fn == null) + throw new ArgumentNullException(nameof(fn)); + + return condition() ? fn(list) : list; + } + + /// + /// Whens the specified condition. + /// + /// The type of IEnumerable. + /// The list. + /// The condition. + /// The function. + /// + /// The IEnumerable. + /// + /// + /// this + /// or + /// condition + /// or + /// fn. + /// + public static IEnumerable When( + this IEnumerable list, + Func condition, + Func, IEnumerable> fn) + { + if (list == null) + throw new ArgumentNullException(nameof(list)); + + if (condition == null) + throw new ArgumentNullException(nameof(condition)); + + if (fn == null) + throw new ArgumentNullException(nameof(fn)); + + return condition() ? fn(list) : list; + } + + /// + /// Adds the value when the condition is true. + /// + /// The type of IList element. + /// The list. + /// The condition. + /// The value. + /// + /// The IList. + /// + /// + /// this + /// or + /// condition + /// or + /// value. + /// + public static IList AddWhen( + this IList list, + Func condition, + Func value) + { + if (list == null) + throw new ArgumentNullException(nameof(list)); + + if (condition == null) + throw new ArgumentNullException(nameof(condition)); + + if (value == null) + throw new ArgumentNullException(nameof(value)); + + if (condition()) + list.Add(value()); + + return list; + } + + /// + /// Adds the value when the condition is true. + /// + /// The type of IList element. + /// The list. + /// if set to true [condition]. + /// The value. + /// + /// The IList. + /// + /// list. + public static IList AddWhen( + this IList list, + bool condition, + T value) + { + if (list == null) + throw new ArgumentNullException(nameof(list)); + + if (condition) + list.Add(value); + + return list; + } + + /// + /// Adds the range when the condition is true. + /// + /// The type of List element. + /// The list. + /// The condition. + /// The value. + /// + /// The List. + /// + /// + /// this + /// or + /// condition + /// or + /// value. + /// + public static List AddRangeWhen( + this List list, + Func condition, + Func> value) + { + if (list == null) + throw new ArgumentNullException(nameof(list)); + + if (condition == null) + throw new ArgumentNullException(nameof(condition)); + + if (value == null) + throw new ArgumentNullException(nameof(value)); + + if (condition()) + list.AddRange(value()); + + return list; + } + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Extensions.IEnumerable.cs b/Vendor/Swan.Lite-3.1.0/Extensions.IEnumerable.cs new file mode 100644 index 0000000..ed36a70 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Extensions.IEnumerable.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Swan +{ + /// + /// This class contains extensions methods for IEnumerable + /// + public static class IEnumerableExtensions + { + /// + /// This method make an union of two IEnumerables + /// validation when some of them is null. + /// + /// T + /// The this. + /// The second. + /// The Union + public static IEnumerable UnionNull(this IEnumerable @this, IEnumerable second) + { + if (@this == null) return second; + return second == null ? @this : @this.Union(second); + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Extensions.IPropertyProxy.cs b/Vendor/Swan.Lite-3.1.0/Extensions.IPropertyProxy.cs new file mode 100644 index 0000000..7b48136 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Extensions.IPropertyProxy.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using Swan.Reflection; + +namespace Swan +{ + /// + /// Provides functionality to access objects + /// associated with types. Getters and setters are stored as delegates compiled + /// from constructed lambda expressions for fast access. + /// + public static class PropertyProxyExtensions + { + private static readonly object SyncLock = new object(); + private static readonly Dictionary> ProxyCache = + new Dictionary>(32); + + /// + /// Gets the property proxies associated with a given type. + /// + /// The type to retrieve property proxies from. + /// A dictionary with property names as keys and objects as values. + public static Dictionary PropertyProxies(this Type t) + { + if (t == null) + throw new ArgumentNullException(nameof(t)); + + lock (SyncLock) + { + if (ProxyCache.ContainsKey(t)) + return ProxyCache[t]; + + var properties = t.GetProperties(BindingFlags.Instance | BindingFlags.Public); + var result = new Dictionary(properties.Length, StringComparer.InvariantCultureIgnoreCase); + foreach (var propertyInfo in properties) + result[propertyInfo.Name] = new PropertyInfoProxy(t, propertyInfo); + + ProxyCache[t] = result; + return result; + } + } + + /// + /// Gets the property proxies associated with the provided instance type. + /// + /// The instance type. + /// The instance. + /// A dictionary with property names as keys and objects as values. + public static Dictionary PropertyProxies(this T obj) => + (obj?.GetType() ?? typeof(T)).PropertyProxies(); + + /// + /// Gets the property proxy given the property name. + /// + /// The associated type. + /// Name of the property. + /// The associated + public static IPropertyProxy PropertyProxy(this Type t, string propertyName) + { + var proxies = t.PropertyProxies(); + return proxies.ContainsKey(propertyName) ? proxies[propertyName] : null; + } + + /// + /// Gets the property proxy given the property name. + /// + /// The type of instance to extract proxies from. + /// The instance to extract proxies from. + /// Name of the property. + /// The associated + public static IPropertyProxy PropertyProxy(this T obj, string propertyName) + { + if (propertyName == null) + throw new ArgumentNullException(nameof(propertyName)); + var proxies = (obj?.GetType() ?? typeof(T)).PropertyProxies(); + + return proxies?.ContainsKey(propertyName) == true ? proxies[propertyName] : null; + } + + /// + /// Gets the property proxy given the property name as an expression. + /// + /// The instance type. + /// The property value type. + /// The object. + /// The property expression. + /// The associated + public static IPropertyProxy PropertyProxy(this T obj, Expression> propertyExpression) + { + if (propertyExpression == null) + throw new ArgumentNullException(nameof(propertyExpression)); + + var proxies = (obj?.GetType() ?? typeof(T)).PropertyProxies(); + var propertyName = propertyExpression.PropertyName(); + return proxies?.ContainsKey(propertyName) == true ? proxies[propertyName] : null; + } + + /// + /// Reads the property value. + /// + /// The type to get property proxies from. + /// The type of the property. + /// The instance. + /// The property expression. + /// + /// The value obtained from the associated + /// + /// obj. + public static V ReadProperty(this T obj, Expression> propertyExpression) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + + var proxy = obj.PropertyProxy(propertyExpression); + return (V)(proxy?.GetValue(obj)); + } + + /// + /// Reads the property value. + /// + /// The type to get property proxies from. + /// The instance. + /// Name of the property. + /// + /// The value obtained from the associated + /// + /// obj. + public static object? ReadProperty(this T obj, string propertyName) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + + var proxy = obj.PropertyProxy(propertyName); + return proxy?.GetValue(obj); + } + + /// + /// Writes the property value. + /// + /// The type to get property proxies from. + /// The type of the property. + /// The instance. + /// The property expression. + /// The value. + public static void WriteProperty(this T obj, Expression> propertyExpression, TV value) + { + var proxy = obj.PropertyProxy(propertyExpression); + proxy?.SetValue(obj, value); + } + + /// + /// Writes the property value using the property proxy. + /// + /// The type to get property proxies from. + /// The instance. + /// Name of the property. + /// The value. + public static void WriteProperty(this T obj, string propertyName, object? value) + { + var proxy = obj.PropertyProxy(propertyName); + proxy?.SetValue(obj, value); + } + + private static string PropertyName(this Expression> propertyExpression) + { + var memberExpression = !(propertyExpression.Body is MemberExpression) + ? (propertyExpression.Body as UnaryExpression).Operand as MemberExpression + : propertyExpression.Body as MemberExpression; + + return memberExpression.Member.Name; + } + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Extensions.Reflection.cs b/Vendor/Swan.Lite-3.1.0/Extensions.Reflection.cs new file mode 100644 index 0000000..18c1b41 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Extensions.Reflection.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Swan +{ + /// + /// Provides various extension methods for Reflection and Types. + /// + public static class ReflectionExtensions + { + /// + /// Gets all types within an assembly in a safe manner. + /// + /// The assembly. + /// + /// Array of Type objects representing the types specified by an assembly. + /// + /// assembly. + public static IEnumerable GetAllTypes(this Assembly assembly) + { + if (assembly == null) + throw new ArgumentNullException(nameof(assembly)); + + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException e) + { + return e.Types.Where(t => t != null); + } + } + + #region Type Extensions + + /// + /// The closest programmatic equivalent of default(T). + /// + /// The type. + /// + /// Default value of this type. + /// + /// type. + public static object? GetDefault(this Type type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + return type.IsValueType ? Activator.CreateInstance(type) : default; + } + + /// + /// Determines whether this type is compatible with ICollection. + /// + /// The type. + /// + /// true if the specified source type is collection; otherwise, false. + /// + /// sourceType. + public static bool IsCollection(this Type sourceType) + { + if (sourceType == null) + throw new ArgumentNullException(nameof(sourceType)); + + return sourceType != typeof(string) && + typeof(IEnumerable).IsAssignableFrom(sourceType); + } + + /// + /// Gets a method from a type given the method name, binding flags, generic types and parameter types. + /// + /// Type of the source. + /// The binding flags. + /// Name of the method. + /// The generic types. + /// The parameter types. + /// + /// An object that represents the method with the specified name. + /// + /// + /// The exception that is thrown when binding to a member results in more than one member matching the + /// binding criteria. This class cannot be inherited. + /// + public static MethodInfo GetMethod( + this Type type, + BindingFlags bindingFlags, + string methodName, + Type[] genericTypes, + Type[] parameterTypes) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (methodName == null) + throw new ArgumentNullException(nameof(methodName)); + + if (genericTypes == null) + throw new ArgumentNullException(nameof(genericTypes)); + + if (parameterTypes == null) + throw new ArgumentNullException(nameof(parameterTypes)); + + var methods = type + .GetMethods(bindingFlags) + .Where(mi => string.Equals(methodName, mi.Name, StringComparison.Ordinal)) + .Where(mi => mi.ContainsGenericParameters) + .Where(mi => mi.GetGenericArguments().Length == genericTypes.Length) + .Where(mi => mi.GetParameters().Length == parameterTypes.Length) + .Select(mi => mi.MakeGenericMethod(genericTypes)) + .Where(mi => mi.GetParameters().Select(pi => pi.ParameterType).SequenceEqual(parameterTypes)) + .ToList(); + + return methods.Count > 1 ? throw new AmbiguousMatchException() : methods.FirstOrDefault(); + } + + /// + /// Determines whether [is i enumerable request]. + /// + /// The type. + /// + /// true if [is i enumerable request] [the specified type]; otherwise, false. + /// + /// type. + public static bool IsIEnumerable(this Type type) + => type == null + ? throw new ArgumentNullException(nameof(type)) + : type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>); + + #endregion + + /// + /// Tries to parse using the basic types. + /// + /// The type. + /// The value. + /// The result. + /// + /// true if parsing was successful; otherwise, false. + /// + /// type + public static bool TryParseBasicType(this Type type, object value, out object? result) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (type != typeof(bool)) + return TryParseBasicType(type, value.ToStringInvariant(), out result); + + result = value.ToBoolean(); + return true; + } + + /// + /// Tries to parse using the basic types. + /// + /// The type. + /// The value. + /// The result. + /// + /// true if parsing was successful; otherwise, false. + /// + /// type + public static bool TryParseBasicType(this Type type, string value, out object? result) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + result = null; + + return Definitions.BasicTypesInfo.Value.ContainsKey(type) && Definitions.BasicTypesInfo.Value[type].TryParse(value, out result); + } + + /// + /// Tries the type of the set basic value to a property. + /// + /// The property information. + /// The value. + /// The object. + /// + /// true if parsing was successful; otherwise, false. + /// + /// propertyInfo. + public static bool TrySetBasicType(this PropertyInfo propertyInfo, object value, object target) + { + if (propertyInfo == null) + throw new ArgumentNullException(nameof(propertyInfo)); + + try + { + if (propertyInfo.PropertyType.TryParseBasicType(value, out var propertyValue)) + { + propertyInfo.SetValue(target, propertyValue); + return true; + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // swallow + } + + return false; + } + + /// + /// Tries the type of the set to an array a basic type. + /// + /// The type. + /// The value. + /// The array. + /// The index. + /// + /// true if parsing was successful; otherwise, false. + /// + /// type + public static bool TrySetArrayBasicType(this Type type, object value, Array target, int index) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (target == null) + return false; + + try + { + if (value == null) + { + target.SetValue(null, index); + return true; + } + + if (type.TryParseBasicType(value, out var propertyValue)) + { + target.SetValue(propertyValue, index); + return true; + } + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + target.SetValue(null, index); + return true; + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // swallow + } + + return false; + } + + /// + /// Tries to set a property array with another array. + /// + /// The property. + /// The value. + /// The object. + /// + /// true if parsing was successful; otherwise, false. + /// + /// propertyInfo. + public static bool TrySetArray(this PropertyInfo propertyInfo, IEnumerable? value, object obj) + { + if (propertyInfo == null) + throw new ArgumentNullException(nameof(propertyInfo)); + + var elementType = propertyInfo.PropertyType.GetElementType(); + + if (elementType == null || value == null) + return false; + + var targetArray = Array.CreateInstance(elementType, value.Count()); + + var i = 0; + + foreach (var sourceElement in value) + { + var result = elementType.TrySetArrayBasicType(sourceElement, targetArray, i++); + + if (!result) return false; + } + + propertyInfo.SetValue(obj, targetArray); + + return true; + } + + /// + /// Convert a string to a boolean. + /// + /// The string. + /// + /// true if the string represents a valid truly value, otherwise false. + /// + public static bool ToBoolean(this string str) + { + try + { + return Convert.ToBoolean(str); + } + catch (FormatException) + { + // ignored + } + + try + { + return Convert.ToBoolean(Convert.ToInt32(str)); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // ignored + } + + return false; + } + + /// + /// Convert a object to a boolean. + /// + /// The value. + /// + /// true if the string represents a valid truly value, otherwise false. + /// + public static bool ToBoolean(this object value) => value.ToStringInvariant().ToBoolean(); + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Extensions.Strings.cs b/Vendor/Swan.Lite-3.1.0/Extensions.Strings.cs new file mode 100644 index 0000000..b765f4d --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Extensions.Strings.cs @@ -0,0 +1,400 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Swan.Formatters; + +namespace Swan +{ + /// + /// String related extension methods. + /// + public static class StringExtensions + { + #region Private Declarations + + private const RegexOptions StandardRegexOptions = + RegexOptions.Multiline | RegexOptions.Compiled | RegexOptions.CultureInvariant; + + private static readonly string[] ByteSuffixes = { "B", "KB", "MB", "GB", "TB" }; + + private static readonly Lazy SplitLinesRegex = + new Lazy(() => new Regex("\r\n|\r|\n", StandardRegexOptions)); + + private static readonly Lazy UnderscoreRegex = + new Lazy(() => new Regex(@"_", StandardRegexOptions)); + + private static readonly Lazy CamelCaseRegEx = + new Lazy(() => new Regex(@"[a-z][A-Z]", StandardRegexOptions)); + + private static readonly Lazy SplitCamelCaseString = new Lazy(() => m => + { + var x = m.ToString(); + return x[0] + " " + x.Substring(1, x.Length - 1); + }); + + private static readonly Lazy InvalidFilenameChars = + new Lazy(() => Path.GetInvalidFileNameChars().Select(c => c.ToStringInvariant()).ToArray()); + + #endregion + + /// + /// Returns a string that represents the given item + /// It tries to use InvariantCulture if the ToString(IFormatProvider) + /// overload exists. + /// + /// The item. + /// A that represents the current object. + public static string ToStringInvariant(this object? @this) + { + if (@this == null) + return string.Empty; + + var itemType = @this.GetType(); + + if (itemType == typeof(string)) + return @this as string ?? string.Empty; + + return Definitions.BasicTypesInfo.Value.ContainsKey(itemType) + ? Definitions.BasicTypesInfo.Value[itemType].ToStringInvariant(@this) + : @this.ToString(); + } + + /// + /// Returns a string that represents the given item + /// It tries to use InvariantCulture if the ToString(IFormatProvider) + /// overload exists. + /// + /// The type to get the string. + /// The item. + /// A that represents the current object. + public static string ToStringInvariant(this T item) + => typeof(string) == typeof(T) ? item as string ?? string.Empty : ToStringInvariant(item as object); + + /// + /// Removes the control characters from a string except for those specified. + /// + /// The input. + /// When specified, these characters will not be removed. + /// + /// A string that represents the current object. + /// + /// input. + public static string RemoveControlChars(this string value, params char[]? excludeChars) + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + if (excludeChars == null) + excludeChars = Array.Empty(); + + return new string(value + .Where(c => char.IsControl(c) == false || excludeChars.Contains(c)) + .ToArray()); + } + + /// + /// Outputs JSON string representing this object. + /// + /// The object. + /// if set to true format the output. + /// A that represents the current object. + public static string ToJson(this object @this, bool format = true) => + @this == null ? string.Empty : Json.Serialize(@this, format); + + /// + /// Returns text representing the properties of the specified object in a human-readable format. + /// While this method is fairly expensive computationally speaking, it provides an easy way to + /// examine objects. + /// + /// The object. + /// A that represents the current object. + public static string Stringify(this object @this) + { + if (@this == null) + return "(null)"; + + try + { + var jsonText = Json.Serialize(@this, false, "$type"); + var jsonData = Json.Deserialize(jsonText); + + return new HumanizeJson(jsonData, 0).GetResult(); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + return @this.ToStringInvariant(); + } + } + + /// + /// Retrieves a section of the string, inclusive of both, the start and end indexes. + /// This behavior is unlike JavaScript's Slice behavior where the end index is non-inclusive + /// If the string is null it returns an empty string. + /// + /// The string. + /// The start index. + /// The end index. + /// Retrieves a substring from this instance. + public static string Slice(this string @this, int startIndex, int endIndex) + { + if (@this == null) + return string.Empty; + + var end = endIndex.Clamp(startIndex, @this.Length - 1); + + return startIndex >= end ? string.Empty : @this.Substring(startIndex, (end - startIndex) + 1); + } + + /// + /// Gets a part of the string clamping the length and startIndex parameters to safe values. + /// If the string is null it returns an empty string. This is basically just a safe version + /// of string.Substring. + /// + /// The string. + /// The start index. + /// The length. + /// Retrieves a substring from this instance. + public static string SliceLength(this string @this, int startIndex, int length) + { + if (@this == null) + return string.Empty; + + var start = startIndex.Clamp(0, @this.Length - 1); + var len = length.Clamp(0, @this.Length - start); + + return len == 0 ? string.Empty : @this.Substring(start, len); + } + + /// + /// Splits the specified text into r, n or rn separated lines. + /// + /// The text. + /// + /// An array whose elements contain the substrings from this instance + /// that are delimited by one or more characters in separator. + /// + public static string[] ToLines(this string @this) => + @this == null ? Array.Empty() : SplitLinesRegex.Value.Split(@this); + + /// + /// Humanizes (make more human-readable) an identifier-style string + /// in either camel case or snake case. For example, CamelCase will be converted to + /// Camel Case and Snake_Case will be converted to Snake Case. + /// + /// The identifier-style string. + /// A humanized. + public static string Humanize(this string value) + { + if (value == null) + return string.Empty; + + var returnValue = UnderscoreRegex.Value.Replace(value, " "); + returnValue = CamelCaseRegEx.Value.Replace(returnValue, SplitCamelCaseString.Value); + return returnValue; + } + + /// + /// Humanizes (make more human-readable) an boolean. + /// + /// if set to true [value]. + /// A that represents the current boolean. + public static string Humanize(this bool value) => value ? "Yes" : "No"; + + /// + /// Humanizes (make more human-readable) the specified value. + /// + /// The value. + /// A that represents the current object. + public static string Humanize(this object value) => + value switch + { + string stringValue => stringValue.Humanize(), + bool boolValue => boolValue.Humanize(), + _ => value.Stringify() + }; + + /// + /// Indents the specified multi-line text with the given amount of leading spaces + /// per line. + /// + /// The text. + /// The spaces. + /// A that represents the current object. + public static string Indent(this string value, int spaces = 4) + { + if (value == null) value = string.Empty; + if (spaces <= 0) return value; + + var lines = value.ToLines(); + var builder = new StringBuilder(); + var indentStr = new string(' ', spaces); + + foreach (var line in lines) + { + builder.AppendLine($"{indentStr}{line}"); + } + + return builder.ToString().TrimEnd(); + } + + /// + /// Gets the line and column number (i.e. not index) of the + /// specified character index. Useful to locate text in a multi-line + /// string the same way a text editor does. + /// Please not that the tuple contains first the line number and then the + /// column number. + /// + /// The string. + /// Index of the character. + /// A 2-tuple whose value is (item1, item2). + public static Tuple TextPositionAt(this string value, int charIndex) + { + if (value == null) + return Tuple.Create(0, 0); + + var index = charIndex.Clamp(0, value.Length - 1); + + var lineIndex = 0; + var colNumber = 0; + + for (var i = 0; i <= index; i++) + { + if (value[i] == '\n') + { + lineIndex++; + colNumber = 0; + continue; + } + + if (value[i] != '\r') + colNumber++; + } + + return Tuple.Create(lineIndex + 1, colNumber); + } + + /// + /// Makes the file name system safe. + /// + /// The s. + /// + /// A string with a safe file name. + /// + /// s. + public static string ToSafeFilename(this string value) => + value == null + ? throw new ArgumentNullException(nameof(value)) + : InvalidFilenameChars.Value + .Aggregate(value, (current, c) => current.Replace(c, string.Empty)) + .Slice(0, 220); + + /// + /// Formats a long into the closest bytes string. + /// + /// The bytes length. + /// + /// The string representation of the current Byte object, formatted as specified by the format parameter. + /// + public static string FormatBytes(this long bytes) => ((ulong)bytes).FormatBytes(); + + /// + /// Formats a long into the closest bytes string. + /// + /// The bytes length. + /// + /// A copy of format in which the format items have been replaced by the string + /// representations of the corresponding arguments. + /// + public static string FormatBytes(this ulong bytes) + { + int i; + double dblSByte = bytes; + + for (i = 0; i < ByteSuffixes.Length && bytes >= 1024; i++, bytes /= 1024) + { + dblSByte = bytes / 1024.0; + } + + return $"{dblSByte:0.##} {ByteSuffixes[i]}"; + } + + /// + /// Truncates the specified value. + /// + /// The value. + /// The maximum length. + /// + /// Retrieves a substring from this instance. + /// The substring starts at a specified character position and has a specified length. + /// + public static string? Truncate(this string value, int maximumLength) => + Truncate(value, maximumLength, string.Empty); + + /// + /// Truncates the specified value and append the omission last. + /// + /// The value. + /// The maximum length. + /// The omission. + /// + /// Retrieves a substring from this instance. + /// The substring starts at a specified character position and has a specified length. + /// + public static string? Truncate(this string value, int maximumLength, string omission) + { + if (value == null) + return null; + + return value.Length > maximumLength + ? value.Substring(0, maximumLength) + (omission ?? string.Empty) + : value; + } + + /// + /// Determines whether the specified contains any of characters in + /// the specified array of . + /// + /// + /// true if contains any of ; + /// otherwise, false. + /// + /// + /// A to test. + /// + /// + /// An array of that contains characters to find. + /// + public static bool Contains(this string value, params char[] chars) => + chars != null && + (chars.Length == 0 || (!string.IsNullOrEmpty(value) && value.IndexOfAny(chars) > -1)); + + /// + /// Replaces all chars in a string. + /// + /// The value. + /// The replace value. + /// The chars. + /// The string with the characters replaced. + public static string ReplaceAll(this string value, string replaceValue, params char[] chars) => + chars.Aggregate(value, (current, c) => current.Replace(new string(new[] { c }), replaceValue)); + + /// + /// Convert hex character to an integer. Return -1 if char is something + /// other than a hex char. + /// + /// The c. + /// Converted integer. + public static int Hex2Int(this char value) => + value >= '0' && value <= '9' + ? value - '0' + : value >= 'A' && value <= 'F' + ? value - 'A' + 10 + : value >= 'a' && value <= 'f' + ? value - 'a' + 10 + : -1; + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Extensions.Tasks.cs b/Vendor/Swan.Lite-3.1.0/Extensions.Tasks.cs new file mode 100644 index 0000000..10391c2 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Extensions.Tasks.cs @@ -0,0 +1,85 @@ +using System; +using System.Threading.Tasks; + +namespace Swan +{ + /// + /// Provides extension methods for and . + /// + public static class TaskExtensions + { + /// + /// Suspends execution until the specified is completed. + /// This method operates similarly to the C# operator, + /// but is meant to be called from a non- method. + /// + /// The on which this method is called. + /// is . + public static void Await(this Task @this) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + @this.GetAwaiter().GetResult(); + } + + /// + /// Suspends execution until the specified is completed + /// and returns its result. + /// This method operates similarly to the C# operator, + /// but is meant to be called from a non- method. + /// + /// The type of the task's result. + /// The on which this method is called. + /// The result of . + /// is . + public static TResult Await(this Task @this) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + return @this.GetAwaiter().GetResult(); + } + + /// + /// Suspends execution until the specified is completed. + /// This method operates similarly to the C# operator, + /// but is meant to be called from a non- method. + /// + /// The on which this method is called. + /// If set to , + /// attempts to marshal the continuation back to the original context captured. + /// This parameter has the same effect as calling the + /// method. + /// is . + public static void Await(this Task @this, bool continueOnCapturedContext) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + @this.ConfigureAwait(continueOnCapturedContext).GetAwaiter().GetResult(); + } + + /// + /// Suspends execution until the specified is completed + /// and returns its result. + /// This method operates similarly to the C# operator, + /// but is meant to be called from a non- method. + /// + /// The type of the task's result. + /// The on which this method is called. + /// If set to , + /// attempts to marshal the continuation back to the original context captured. + /// This parameter has the same effect as calling the + /// method. + /// The result of . + /// is . + public static TResult Await(this Task @this, bool continueOnCapturedContext) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + return @this.ConfigureAwait(continueOnCapturedContext).GetAwaiter().GetResult(); + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Extensions.ValueTypes.cs b/Vendor/Swan.Lite-3.1.0/Extensions.ValueTypes.cs new file mode 100644 index 0000000..91051f7 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Extensions.ValueTypes.cs @@ -0,0 +1,165 @@ +using System; +using System.Reflection; +using System.Runtime.InteropServices; +using Swan.Reflection; + +namespace Swan +{ + /// + /// Provides various extension methods for value types and structs. + /// + public static class ValueTypeExtensions + { + /// + /// Clamps the specified value between the minimum and the maximum. + /// + /// The type of value to clamp. + /// The value. + /// The minimum. + /// The maximum. + /// A value that indicates the relative order of the objects being compared. + public static T Clamp(this T @this, T min, T max) + where T : struct, IComparable + { + if (@this.CompareTo(min) < 0) return min; + + return @this.CompareTo(max) > 0 ? max : @this; + } + + /// + /// Clamps the specified value between the minimum and the maximum. + /// + /// The value. + /// The minimum. + /// The maximum. + /// A value that indicates the relative order of the objects being compared. + public static int Clamp(this int @this, int min, int max) + => @this < min ? min : (@this > max ? max : @this); + + /// + /// Determines whether the specified value is between a minimum and a maximum value. + /// + /// The type of value to check. + /// The value. + /// The minimum. + /// The maximum. + /// + /// true if the specified minimum is between; otherwise, false. + /// + public static bool IsBetween(this T @this, T min, T max) + where T : struct, IComparable + { + return @this.CompareTo(min) >= 0 && @this.CompareTo(max) <= 0; + } + + /// + /// Converts an array of bytes into the given struct type. + /// + /// The type of structure to convert. + /// The data. + /// a struct type derived from convert an array of bytes ref=ToStruct". + public static T ToStruct(this byte[] @this) + where T : struct + { + return @this == null ? throw new ArgumentNullException(nameof(@this)) : ToStruct(@this, 0, @this.Length); + } + + /// + /// Converts an array of bytes into the given struct type. + /// + /// The type of structure to convert. + /// The data. + /// The offset. + /// The length. + /// + /// A managed object containing the data pointed to by the ptr parameter. + /// + /// data. + public static T ToStruct(this byte[] @this, int offset, int length) + where T : struct + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + var buffer = new byte[length]; + Array.Copy(@this, offset, buffer, 0, buffer.Length); + var handle = GCHandle.Alloc(GetStructBytes(buffer), GCHandleType.Pinned); + + try + { + return Marshal.PtrToStructure(handle.AddrOfPinnedObject()); + } + finally + { + handle.Free(); + } + } + + /// + /// Converts a struct to an array of bytes. + /// + /// The type of structure to convert. + /// The object. + /// A byte array containing the results of encoding the specified set of characters. + public static byte[] ToBytes(this T @this) + where T : struct + { + var data = new byte[Marshal.SizeOf(@this)]; + var handle = GCHandle.Alloc(data, GCHandleType.Pinned); + + try + { + Marshal.StructureToPtr(@this, handle.AddrOfPinnedObject(), false); + return GetStructBytes(data); + } + finally + { + handle.Free(); + } + } + + /// + /// Swaps the endianness of an unsigned long to an unsigned integer. + /// + /// The bytes contained in a long. + /// + /// A 32-bit unsigned integer equivalent to the ulong + /// contained in longBytes. + /// + public static uint SwapEndianness(this ulong @this) + => (uint)(((@this & 0x000000ff) << 24) + + ((@this & 0x0000ff00) << 8) + + ((@this & 0x00ff0000) >> 8) + + ((@this & 0xff000000) >> 24)); + + private static byte[] GetStructBytes(byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var fields = typeof(T).GetTypeInfo() + .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + var endian = AttributeCache.DefaultCache.Value.RetrieveOne(); + + foreach (var field in fields) + { + if (endian == null && !field.IsDefined(typeof(StructEndiannessAttribute), false)) + continue; + + var offset = Marshal.OffsetOf(field.Name).ToInt32(); + var length = Marshal.SizeOf(field.FieldType); + + endian ??= AttributeCache.DefaultCache.Value.RetrieveOne(field); + + if (endian != null && (endian.Endianness == Endianness.Big && BitConverter.IsLittleEndian || + endian.Endianness == Endianness.Little && !BitConverter.IsLittleEndian)) + { + Array.Reverse(data, offset, length); + } + } + + return data; + } + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Extensions.cs b/Vendor/Swan.Lite-3.1.0/Extensions.cs new file mode 100644 index 0000000..1bc2a33 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Extensions.cs @@ -0,0 +1,279 @@ +using Swan.Formatters; +using Swan.Mappers; +using Swan.Reflection; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Swan +{ + /// + /// Extension methods. + /// + public static partial class Extensions + { + /// + /// Iterates over the public, instance, readable properties of the source and + /// tries to write a compatible value to a public, instance, writable property in the destination. + /// + /// The type of the source. + /// The source. + /// The target. + /// The ignore properties. + /// + /// Number of properties that was copied successful. + /// + public static int CopyPropertiesTo(this T source, object target, params string[]? ignoreProperties) + where T : class => + ObjectMapper.Copy(source, target, GetCopyableProperties(target), ignoreProperties); + + /// + /// Iterates over the public, instance, readable properties of the source and + /// tries to write a compatible value to a public, instance, writable property in the destination. + /// + /// The source. + /// The destination. + /// Properties to copy. + /// + /// Number of properties that were successfully copied. + /// + public static int CopyOnlyPropertiesTo(this object source, object target, params string[]? propertiesToCopy) + => ObjectMapper.Copy(source, target, propertiesToCopy); + + /// + /// Copies the properties to new instance of T. + /// + /// The new object type. + /// The source. + /// The ignore properties. + /// + /// The specified type with properties copied. + /// + /// source. + public static T CopyPropertiesToNew(this object source, string[]? ignoreProperties = null) + where T : class + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + var target = Activator.CreateInstance(); + ObjectMapper.Copy(source, target, GetCopyableProperties(target), ignoreProperties); + + return target; + } + + /// + /// Copies the only properties to new instance of T. + /// + /// Object Type. + /// The source. + /// The properties to copy. + /// + /// The specified type with properties copied. + /// + /// source. + public static T CopyOnlyPropertiesToNew(this object source, params string[] propertiesToCopy) + where T : class + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + var target = Activator.CreateInstance(); + ObjectMapper.Copy(source, target, propertiesToCopy); + + return target; + } + + /// + /// Iterates over the keys of the source and tries to write a compatible value to a public, + /// instance, writable property in the destination. + /// + /// The source. + /// The target. + /// The ignore keys. + /// Number of properties that was copied successful. + public static int CopyKeyValuePairTo( + this IDictionary source, + object? target, + params string[] ignoreKeys) => + source == null + ? throw new ArgumentNullException(nameof(source)) + : ObjectMapper.Copy(source, target, null, ignoreKeys); + + /// + /// Iterates over the keys of the source and tries to write a compatible value to a public, + /// instance, writable property in the destination. + /// + /// Object Type. + /// The source. + /// The ignore keys. + /// + /// The specified type with properties copied. + /// + public static T CopyKeyValuePairToNew( + this IDictionary source, + params string[] ignoreKeys) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + var target = Activator.CreateInstance(); + source.CopyKeyValuePairTo(target, ignoreKeys); + return target; + } + + /// + /// Does the specified action. + /// + /// The action. + /// The retry interval. + /// The retry count. + public static void Retry( + this Action action, + TimeSpan retryInterval = default, + int retryCount = 3) + { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + Retry(() => + { + action(); + return null; + }, + retryInterval, + retryCount); + } + + /// + /// Does the specified action. + /// + /// The type of the source. + /// The action. + /// The retry interval. + /// The retry count. + /// + /// The return value of the method that this delegate encapsulates. + /// + /// action. + /// Represents one or many errors that occur during application execution. + public static T Retry( + this Func action, + TimeSpan retryInterval = default, + int retryCount = 3) + { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + if (retryInterval == default) + retryInterval = TimeSpan.FromSeconds(1); + + var exceptions = new List(); + + for (var retry = 0; retry < retryCount; retry++) + { + try + { + if (retry > 0) + Task.Delay(retryInterval).Wait(); + + return action(); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + exceptions.Add(ex); + } + } + + throw new AggregateException(exceptions); + } + + /// + /// Gets the copyable properties. + /// + /// If there is no properties with the attribute AttributeCache returns all the properties. + /// + /// The object. + /// + /// Array of properties. + /// + /// model. + /// + public static IEnumerable GetCopyableProperties(this object @this) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + var collection = PropertyTypeCache.DefaultCache.Value + .RetrieveAllProperties(@this.GetType(), true); + + var properties = collection + .Select(x => new + { + x.Name, + HasAttribute = AttributeCache.DefaultCache.Value.RetrieveOne(x) != null, + }) + .Where(x => x.HasAttribute) + .Select(x => x.Name); + + return properties.Any() + ? properties + : collection.Select(x => x.Name); + } + + internal static void CreateTarget( + this object source, + Type targetType, + bool includeNonPublic, + ref object? target) + { + switch (source) + { + // do nothing. Simply skip creation + case string _: + break; + + // When using arrays, there is no default constructor, attempt to build a compatible array + case IList sourceObjectList when targetType.IsArray: + var elementType = targetType.GetElementType(); + + if (elementType != null) + target = Array.CreateInstance(elementType, sourceObjectList.Count); + break; + default: + var constructors = ConstructorTypeCache.DefaultCache.Value + .RetrieveAllConstructors(targetType, includeNonPublic); + + // Try to check if empty constructor is available + if (constructors.Any(x => x.Item2.Length == 0)) + { + target = Activator.CreateInstance(targetType, includeNonPublic); + } + else + { + var firstCtor = constructors + .OrderBy(x => x.Item2.Length) + .FirstOrDefault(); + + target = Activator.CreateInstance(targetType, + firstCtor?.Item2.Select(arg => arg.GetType().GetDefault()).ToArray()); + } + + break; + } + } + + internal static string GetNameWithCase(this string name, JsonSerializerCase jsonSerializerCase) => + jsonSerializerCase switch + { + JsonSerializerCase.PascalCase => char.ToUpperInvariant(name[0]) + name.Substring(1), + JsonSerializerCase.CamelCase => char.ToLowerInvariant(name[0]) + name.Substring(1), + JsonSerializerCase.None => name, + _ => throw new ArgumentOutOfRangeException(nameof(jsonSerializerCase), jsonSerializerCase, null) + }; + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Formatters/CsvReader.cs b/Vendor/Swan.Lite-3.1.0/Formatters/CsvReader.cs new file mode 100644 index 0000000..b272337 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Formatters/CsvReader.cs @@ -0,0 +1,647 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Swan.Reflection; + +namespace Swan.Formatters +{ + /// + /// Represents a reader designed for CSV text. + /// It is capable of deserializing objects from individual lines of CSV text, + /// transforming CSV lines of text into objects, + /// or simply reading the lines of CSV as an array of strings. + /// + /// + /// + /// The following example describes how to load a list of objects from a CSV file. + /// + /// using Swan.Formatters; + /// + /// class Example + /// { + /// class Person + /// { + /// public string Name { get; set; } + /// public int Age { get; set; } + /// } + /// + /// static void Main() + /// { + /// // load records from a CSV file + /// var loadedRecords = + /// CsvReader.LoadRecords<Person>("C:\\Users\\user\\Documents\\file.csv"); + /// + /// // loadedRecords = + /// // [ + /// // { Age = 20, Name = "George" } + /// // { Age = 18, Name = "Juan" } + /// // ] + /// } + /// } + /// + /// The following code explains how to read a CSV formatted string. + /// + /// using Swan.Formatters; + /// using System.Text; + /// using Swan.Formatters; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // data to be read + /// var data = @"Company,OpenPositions,MainTechnology,Revenue + /// Co,2,""C#, MySQL, JavaScript, HTML5 and CSS3"",500 + /// Ca,2,""C#, MySQL, JavaScript, HTML5 and CSS3"",600"; + /// + /// using(var stream = new MemoryStream(Encoding.UTF8.GetBytes(data))) + /// { + /// // create a CSV reader + /// var reader = new CsvReader(stream, false, Encoding.UTF8); + /// } + /// } + /// } + /// + /// + public class CsvReader : IDisposable + { + private static readonly PropertyTypeCache TypeCache = new PropertyTypeCache(); + + private readonly object _syncLock = new object(); + + private ulong _count; + private char _escapeCharacter = '"'; + private char _separatorCharacter = ','; + + private bool _hasDisposed; // To detect redundant calls + private string[]? _headings; + private Dictionary? _defaultMap; + private StreamReader? _reader; + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The stream. + /// if set to true leaves the input stream open. + /// The text encoding. + public CsvReader(Stream inputStream, bool leaveOpen, Encoding textEncoding) + { + if (inputStream == null) + throw new ArgumentNullException(nameof(inputStream)); + + if (textEncoding == null) + throw new ArgumentNullException(nameof(textEncoding)); + + _reader = new StreamReader(inputStream, textEncoding, true, 2048, leaveOpen); + } + + /// + /// Initializes a new instance of the class. + /// It will automatically close the stream upon disposing. + /// + /// The stream. + /// The text encoding. + public CsvReader(Stream stream, Encoding textEncoding) + : this(stream, false, textEncoding) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// It automatically closes the stream when disposing this reader + /// and uses the Windows 1253 encoding. + /// + /// The stream. + public CsvReader(Stream stream) + : this(stream, false, Definitions.Windows1252Encoding) + { + } + + /// + /// Initializes a new instance of the class. + /// It uses the Windows 1252 Encoding by default and it automatically closes the file + /// when this reader is disposed of. + /// + /// The filename. + public CsvReader(string filename) + : this(File.OpenRead(filename), false, Definitions.Windows1252Encoding) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// It automatically closes the file when disposing this reader. + /// + /// The filename. + /// The encoding. + public CsvReader(string filename, Encoding encoding) + : this(File.OpenRead(filename), false, encoding) + { + // placeholder + } + + #endregion + + #region Properties + + /// + /// Gets number of lines that have been read, including the headings. + /// + /// + /// The count. + /// + public ulong Count + { + get + { + lock (_syncLock) + { + return _count; + } + } + } + + /// + /// Gets or sets the escape character. + /// By default it is the double quote '"'. + /// + /// + /// The escape character. + /// + public char EscapeCharacter + { + get => _escapeCharacter; + set + { + lock (_syncLock) + { + _escapeCharacter = value; + } + } + } + + /// + /// Gets or sets the separator character. + /// By default it is the comma character ','. + /// + /// + /// The separator character. + /// + public char SeparatorCharacter + { + get => _separatorCharacter; + set + { + lock (_syncLock) + { + _separatorCharacter = value; + } + } + } + + /// + /// Gets a value indicating whether the stream reader is at the end of the stream + /// In other words, if no more data can be read, this will be set to true. + /// + /// + /// true if [end of stream]; otherwise, false. + /// + public bool EndOfStream + { + get + { + lock (_syncLock) + { + return _reader?.EndOfStream ?? true; + } + } + } + + #endregion + + #region Generic, Main ReadLine method + + /// + /// Reads a line of CSV text into an array of strings. + /// + /// An array of the specified element type containing copies of the elements of the ArrayList. + /// Cannot read past the end of the stream. + public string[] ReadLine() + { + if (EndOfStream) + throw new EndOfStreamException("Cannot read past the end of the stream"); + + lock (_syncLock) + { + var values = ParseRecord(_reader!, _escapeCharacter, _separatorCharacter); + _count++; + return values; + } + } + + #endregion + + #region Read Methods + + /// + /// Skips a line of CSV text. + /// This operation does not increment the Count property and it is useful when you need to read the headings + /// skipping over a few lines as Reading headings is only supported + /// as the first read operation (i.e. while count is still 0). + /// + /// Cannot read past the end of the stream. + public void SkipRecord() + { + if (EndOfStream) + throw new EndOfStreamException("Cannot read past the end of the stream"); + + lock (_syncLock) + { + ParseRecord(_reader!, _escapeCharacter, _separatorCharacter); + } + } + + /// + /// Reads a line of CSV text and stores the values read as a representation of the column names + /// to be used for parsing objects. You have to call this method before calling ReadObject methods. + /// + /// An array of the specified element type containing copies of the elements of the ArrayList. + /// + /// Reading headings is only supported as the first read operation. + /// or + /// ReadHeadings. + /// + /// Cannot read past the end of the stream. + public string[] ReadHeadings() + { + lock (_syncLock) + { + if (_headings != null) + throw new InvalidOperationException($"The {nameof(ReadHeadings)} method had already been called."); + + if (_count != 0) + throw new InvalidOperationException("Reading headings is only supported as the first read operation."); + + _headings = ReadLine(); + _defaultMap = _headings.ToDictionary(x => x, x => x); + + return _headings.ToArray(); + } + } + + /// + /// Reads a line of CSV text, converting it into a dynamic object in which properties correspond to the names of the headings. + /// + /// The mappings between CSV headings (keys) and object properties (values). + /// Object of the type of the elements in the collection of key/value pairs. + /// ReadHeadings. + /// Cannot read past the end of the stream. + /// map. + public IDictionary ReadObject(IDictionary map) + { + lock (_syncLock) + { + if (_headings == null) + throw new InvalidOperationException($"Call the {nameof(ReadHeadings)} method before reading as an object."); + + if (map == null) + throw new ArgumentNullException(nameof(map)); + + var result = new Dictionary(); + var values = ReadLine(); + + for (var i = 0; i < _headings.Length; i++) + { + if (i > values.Length - 1) + break; + + result[_headings[i]] = values[i]; + } + + return result; + } + } + + /// + /// Reads a line of CSV text, converting it into a dynamic object + /// The property names correspond to the names of the CSV headings. + /// + /// Object of the type of the elements in the collection of key/value pairs. + public IDictionary ReadObject() => ReadObject(_defaultMap); + + /// + /// Reads a line of CSV text converting it into an object of the given type, using a map (or Dictionary) + /// where the keys are the names of the headings and the values are the names of the instance properties + /// in the given Type. The result object must be already instantiated. + /// + /// The type of object to map. + /// The map. + /// The result. + /// map + /// or + /// result. + /// ReadHeadings. + /// Cannot read past the end of the stream. + public void ReadObject(IDictionary map, ref T result) + { + lock (_syncLock) + { + // Check arguments + { + if (map == null) + throw new ArgumentNullException(nameof(map)); + + if (_reader.EndOfStream) + throw new EndOfStreamException("Cannot read past the end of the stream"); + + if (_headings == null) + throw new InvalidOperationException($"Call the {nameof(ReadHeadings)} method before reading as an object."); + + if (Equals(result, default(T))) + throw new ArgumentNullException(nameof(result)); + } + + // Read line and extract values + var values = ReadLine(); + + // Extract properties from cache + var properties = TypeCache + .RetrieveFilteredProperties(typeof(T), true, x => x.CanWrite && Definitions.BasicTypesInfo.Value.ContainsKey(x.PropertyType)); + + // Assign property values for each heading + for (var i = 0; i < _headings.Length; i++) + { + // break if no more headings are matched + if (i > values.Length - 1) + break; + + // skip if no heading is available or the heading is empty + if (map.ContainsKey(_headings[i]) == false && + string.IsNullOrWhiteSpace(map[_headings[i]]) == false) + continue; + + // Prepare the target property + var propertyName = map[_headings[i]]; + + // Parse and assign the basic type value to the property if exists + properties + .FirstOrDefault(p => p.Name == propertyName)? + .TrySetBasicType(values[i], result); + } + } + } + + /// + /// Reads a line of CSV text converting it into an object of the given type, using a map (or Dictionary) + /// where the keys are the names of the headings and the values are the names of the instance properties + /// in the given Type. + /// + /// The type of object to map. + /// The map of CSV headings (keys) and Type property names (values). + /// The conversion of specific type of object. + /// map. + /// ReadHeadings. + /// Cannot read past the end of the stream. + public T ReadObject(IDictionary map) + where T : new() + { + if (map == null) + throw new ArgumentNullException(nameof(map)); + + var result = Activator.CreateInstance(); + ReadObject(map, ref result); + return result; + } + + /// + /// Reads a line of CSV text converting it into an object of the given type, and assuming + /// the property names of the target type match the heading names of the file. + /// + /// The type of object. + /// The conversion of specific type of object. + public T ReadObject() + where T : new() => + ReadObject(_defaultMap); + + #endregion + + #region Support Methods + + /// + /// Parses a line of standard CSV text into an array of strings. + /// Note that quoted values might have new line sequences in them. Field values will contain such sequences. + /// + /// The reader. + /// The escape character. + /// The separator character. + /// An array of the specified element type containing copies of the elements of the ArrayList. + private static string[] ParseRecord(StreamReader reader, char escapeCharacter = '"', char separatorCharacter = ',') + { + var values = new List(); + var currentValue = new StringBuilder(1024); + var currentState = ReadState.WaitingForNewField; + string line; + + while ((line = reader.ReadLine()) != null) + { + for (var charIndex = 0; charIndex < line.Length; charIndex++) + { + // Get the current and next character + var currentChar = line[charIndex]; + var nextChar = charIndex < line.Length - 1 ? line[charIndex + 1] : default(char?); + + // Perform logic based on state and decide on next state + switch (currentState) + { + case ReadState.WaitingForNewField: + { + currentValue.Clear(); + + if (currentChar == escapeCharacter) + { + currentState = ReadState.PushingQuoted; + continue; + } + + if (currentChar == separatorCharacter) + { + values.Add(currentValue.ToString()); + currentState = ReadState.WaitingForNewField; + continue; + } + + currentValue.Append(currentChar); + currentState = ReadState.PushingNormal; + continue; + } + + case ReadState.PushingNormal: + { + // Handle field content delimiter by comma + if (currentChar == separatorCharacter) + { + currentState = ReadState.WaitingForNewField; + values.Add(currentValue.ToString()); + currentValue.Clear(); + continue; + } + + // Handle double quote escaping + if (currentChar == escapeCharacter && nextChar.HasValue && nextChar == escapeCharacter) + { + // advance 1 character now. The loop will advance one more. + currentValue.Append(currentChar); + charIndex++; + continue; + } + + currentValue.Append(currentChar); + break; + } + + case ReadState.PushingQuoted: + { + // Handle field content delimiter by ending double quotes + if (currentChar == escapeCharacter && (nextChar.HasValue == false || nextChar != escapeCharacter)) + { + currentState = ReadState.PushingNormal; + continue; + } + + // Handle double quote escaping + if (currentChar == escapeCharacter && nextChar.HasValue && nextChar == escapeCharacter) + { + // advance 1 character now. The loop will advance one more. + currentValue.Append(currentChar); + charIndex++; + continue; + } + + currentValue.Append(currentChar); + break; + } + } + } + + // determine if we need to continue reading a new line if it is part of the quoted + // field value + if (currentState == ReadState.PushingQuoted) + { + // we need to add the new line sequence to the output of the field + // because we were pushing a quoted value + currentValue.Append(Environment.NewLine); + } + else + { + // push anything that has not been pushed (flush) into a last value + values.Add(currentValue.ToString()); + currentValue.Clear(); + + // stop reading more lines we have reached the end of the CSV record + break; + } + } + + // If we ended up pushing quoted and no closing quotes we might + // have additional text in yt + if (currentValue.Length > 0) + { + values.Add(currentValue.ToString()); + } + + return values.ToArray(); + } + + #endregion + + #region Helpers + + /// + /// Loads the records from the stream + /// This method uses Windows 1252 encoding. + /// + /// The type of IList items to load. + /// The stream. + /// A generic collection of objects that can be individually accessed by index. + public static IList LoadRecords(Stream stream) + where T : new() + { + var result = new List(); + + using (var reader = new CsvReader(stream)) + { + reader.ReadHeadings(); + while (!reader.EndOfStream) + { + result.Add(reader.ReadObject()); + } + } + + return result; + } + + /// + /// Loads the records from the give file path. + /// This method uses Windows 1252 encoding. + /// + /// The type of IList items to load. + /// The file path. + /// A generic collection of objects that can be individually accessed by index. + public static IList LoadRecords(string filePath) + where T : new() => + LoadRecords(File.OpenRead(filePath)); + + #endregion + + #region IDisposable Support + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_hasDisposed) return; + + if (disposing) + { + try + { + _reader?.Dispose(); + } + finally + { + _reader = null; + } + } + + _hasDisposed = true; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + /// + /// Defines the 3 different read states + /// for the parsing state machine. + /// + private enum ReadState + { + WaitingForNewField, + PushingNormal, + PushingQuoted, + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Formatters/CsvWriter.cs b/Vendor/Swan.Lite-3.1.0/Formatters/CsvWriter.cs new file mode 100644 index 0000000..d2bbe94 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Formatters/CsvWriter.cs @@ -0,0 +1,460 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using Swan.Reflection; + +namespace Swan.Formatters +{ + /// + /// A CSV writer useful for exporting a set of objects. + /// + /// + /// The following code describes how to save a list of objects into a CSV file. + /// + /// using System.Collections.Generic; + /// using Swan.Formatters; + /// + /// class Example + /// { + /// class Person + /// { + /// public string Name { get; set; } + /// public int Age { get; set; } + /// } + /// + /// static void Main() + /// { + /// // create a list of people + /// var people = new List<Person> + /// { + /// new Person { Name = "Artyom", Age = 20 }, + /// new Person { Name = "Aloy", Age = 18 } + /// } + /// + /// // write items inside file.csv + /// CsvWriter.SaveRecords(people, "C:\\Users\\user\\Documents\\file.csv"); + /// + /// // output + /// // | Name | Age | + /// // | Artyom | 20 | + /// // | Aloy | 18 | + /// } + /// } + /// + /// + public class CsvWriter : IDisposable + { + private static readonly PropertyTypeCache TypeCache = new PropertyTypeCache(); + + private readonly object _syncLock = new object(); + private readonly Stream _outputStream; + private readonly Encoding _encoding; + private readonly bool _leaveStreamOpen; + private bool _isDisposing; + private ulong _mCount; + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The output stream. + /// if set to true [leave open]. + /// The encoding. + public CsvWriter(Stream outputStream, bool leaveOpen, Encoding encoding) + { + _outputStream = outputStream; + _encoding = encoding; + _leaveStreamOpen = leaveOpen; + } + + /// + /// Initializes a new instance of the class. + /// It automatically closes the stream when disposing this writer. + /// + /// The output stream. + /// The encoding. + public CsvWriter(Stream outputStream, Encoding encoding) + : this(outputStream, false, encoding) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// It uses the Windows 1252 encoding and automatically closes + /// the stream upon disposing this writer. + /// + /// The output stream. + public CsvWriter(Stream outputStream) + : this(outputStream, false, Definitions.Windows1252Encoding) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// It opens the file given file, automatically closes the stream upon + /// disposing of this writer, and uses the Windows 1252 encoding. + /// + /// The filename. + public CsvWriter(string filename) + : this(File.OpenWrite(filename), false, Definitions.Windows1252Encoding) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// It opens the file given file, automatically closes the stream upon + /// disposing of this writer, and uses the given text encoding for output. + /// + /// The filename. + /// The encoding. + public CsvWriter(string filename, Encoding encoding) + : this(File.OpenWrite(filename), false, encoding) + { + // placeholder + } + + #endregion + + #region Properties + + /// + /// Gets or sets the field separator character. + /// + /// + /// The separator character. + /// + public char SeparatorCharacter { get; set; } = ','; + + /// + /// Gets or sets the escape character to use to escape field values. + /// + /// + /// The escape character. + /// + public char EscapeCharacter { get; set; } = '"'; + + /// + /// Gets or sets the new line character sequence to use when writing a line. + /// + /// + /// The new line sequence. + /// + public string NewLineSequence { get; set; } = Environment.NewLine; + + /// + /// Defines a list of properties to ignore when outputting CSV lines. + /// + /// + /// The ignore property names. + /// + public List IgnorePropertyNames { get; } = new List(); + + /// + /// Gets number of lines that have been written, including the headings line. + /// + /// + /// The count. + /// + public ulong Count + { + get + { + lock (_syncLock) + { + return _mCount; + } + } + } + + #endregion + + #region Helpers + + /// + /// Saves the items to a stream. + /// It uses the Windows 1252 text encoding for output. + /// + /// The type of enumeration. + /// The items. + /// The stream. + /// true if stream is truncated, default false. + /// Number of item saved. + public static int SaveRecords(IEnumerable items, Stream stream, bool truncateData = false) + { + // truncate the file if it had data + if (truncateData && stream.Length > 0) + stream.SetLength(0); + + using var writer = new CsvWriter(stream); + writer.WriteHeadings(); + writer.WriteObjects(items); + return (int)writer.Count; + } + + /// + /// Saves the items to a CSV file. + /// If the file exits, it overwrites it. If it does not, it creates it. + /// It uses the Windows 1252 text encoding for output. + /// + /// The type of enumeration. + /// The items. + /// The file path. + /// Number of item saved. + public static int SaveRecords(IEnumerable items, string filePath) => SaveRecords(items, File.OpenWrite(filePath), true); + + #endregion + + #region Generic, main Write Line Method + + /// + /// Writes a line of CSV text. Items are converted to strings. + /// If items are found to be null, empty strings are written out. + /// If items are not string, the ToStringInvariant() method is called on them. + /// + /// The items. + public void WriteLine(params object[] items) + => WriteLine(items.Select(x => x == null ? string.Empty : x.ToStringInvariant())); + + /// + /// Writes a line of CSV text. Items are converted to strings. + /// If items are found to be null, empty strings are written out. + /// If items are not string, the ToStringInvariant() method is called on them. + /// + /// The items. + public void WriteLine(IEnumerable items) + => WriteLine(items.Select(x => x == null ? string.Empty : x.ToStringInvariant())); + + /// + /// Writes a line of CSV text. + /// If items are found to be null, empty strings are written out. + /// + /// The items. + public void WriteLine(params string[] items) => WriteLine((IEnumerable) items); + + /// + /// Writes a line of CSV text. + /// If items are found to be null, empty strings are written out. + /// + /// The items. + public void WriteLine(IEnumerable items) + { + lock (_syncLock) + { + var length = items.Count(); + var separatorBytes = _encoding.GetBytes(new[] { SeparatorCharacter }); + var endOfLineBytes = _encoding.GetBytes(NewLineSequence); + + // Declare state variables here to avoid recreation, allocation and + // reassignment in every loop + bool needsEnclosing; + string textValue; + byte[] output; + + for (var i = 0; i < length; i++) + { + textValue = items.ElementAt(i); + + // Determine if we need the string to be enclosed + // (it either contains an escape, new line, or separator char) + needsEnclosing = textValue.IndexOf(SeparatorCharacter) >= 0 + || textValue.IndexOf(EscapeCharacter) >= 0 + || textValue.IndexOf('\r') >= 0 + || textValue.IndexOf('\n') >= 0; + + // Escape the escape characters by repeating them twice for every instance + textValue = textValue.Replace($"{EscapeCharacter}", + $"{EscapeCharacter}{EscapeCharacter}"); + + // Enclose the text value if we need to + if (needsEnclosing) + textValue = string.Format($"{EscapeCharacter}{textValue}{EscapeCharacter}", textValue); + + // Get the bytes to write to the stream and write them + output = _encoding.GetBytes(textValue); + _outputStream.Write(output, 0, output.Length); + + // only write a separator if we are moving in between values. + // the last value should not be written. + if (i < length - 1) + _outputStream.Write(separatorBytes, 0, separatorBytes.Length); + } + + // output the newline sequence + _outputStream.Write(endOfLineBytes, 0, endOfLineBytes.Length); + _mCount += 1; + } + } + + #endregion + + #region Write Object Method + + /// + /// Writes a row of CSV text. It handles the special cases where the object is + /// a dynamic object or and array. It also handles non-collection objects fine. + /// If you do not like the way the output is handled, you can simply write an extension + /// method of this class and use the WriteLine method instead. + /// + /// The item. + /// item. + public void WriteObject(object? item) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + + lock (_syncLock) + { + switch (item) + { + case IDictionary typedItem: + WriteLine(GetFilteredDictionary(typedItem)); + return; + case ICollection typedItem: + WriteLine(typedItem.Cast()); + return; + default: + WriteLine(GetFilteredTypeProperties(item.GetType()).Select(x => item.ReadProperty(x.Name))); + break; + } + } + } + + /// + /// Writes a row of CSV text. It handles the special cases where the object is + /// a dynamic object or and array. It also handles non-collection objects fine. + /// If you do not like the way the output is handled, you can simply write an extension + /// method of this class and use the WriteLine method instead. + /// + /// The type of object to write. + /// The item. + public void WriteObject(T item) => WriteObject(item as object); + + /// + /// Writes a set of items, one per line and atomically by repeatedly calling the + /// WriteObject method. For more info check out the description of the WriteObject + /// method. + /// + /// The type of object to write. + /// The items. + /// items. + public void WriteObjects(IEnumerable items) + { + if (items == null) + throw new ArgumentNullException(nameof(items)); + + lock (_syncLock) + { + foreach (var item in items) + WriteObject(item); + } + } + + #endregion + + #region Write Headings Methods + + /// + /// Writes the headings. + /// + /// The type of object to extract headings. + /// type. + public void WriteHeadings(Type type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + var properties = GetFilteredTypeProperties(type).Select(p => p.Name).Cast(); + WriteLine(properties); + } + + /// + /// Writes the headings. + /// + /// The type of object to extract headings. + public void WriteHeadings() => WriteHeadings(typeof(T)); + + /// + /// Writes the headings. + /// + /// The dictionary to extract headings. + /// dictionary. + public void WriteHeadings(IDictionary dictionary) + { + if (dictionary == null) + throw new ArgumentNullException(nameof(dictionary)); + + WriteLine(GetFilteredDictionary(dictionary, true)); + } + + /// + /// Writes the headings. + /// + /// The object to extract headings. + /// obj. + public void WriteHeadings(object obj) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + + WriteHeadings(obj.GetType()); + } + + #endregion + + #region IDisposable Support + + /// + public void Dispose() => Dispose(true); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposeAlsoManaged) + { + if (_isDisposing) return; + + if (disposeAlsoManaged) + { + if (_leaveStreamOpen == false) + { + _outputStream.Dispose(); + } + } + + _isDisposing = true; + } + + #endregion + + #region Support Methods + + private IEnumerable GetFilteredDictionary(IDictionary dictionary, bool filterKeys = false) + => dictionary + .Keys + .Cast() + .Select(key => key == null ? string.Empty : key.ToStringInvariant()) + .Where(stringKey => !IgnorePropertyNames.Contains(stringKey)) + .Select(stringKey => + filterKeys + ? stringKey + : dictionary[stringKey] == null ? string.Empty : dictionary[stringKey].ToStringInvariant()); + + private IEnumerable GetFilteredTypeProperties(Type type) + => TypeCache.Retrieve(type, t => + t.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead)) + .Where(p => !IgnorePropertyNames.Contains(p.Name)); + + #endregion + + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Formatters/HumanizeJson.cs b/Vendor/Swan.Lite-3.1.0/Formatters/HumanizeJson.cs new file mode 100644 index 0000000..600136d --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Formatters/HumanizeJson.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Swan.Formatters +{ + internal class HumanizeJson + { + private readonly StringBuilder _builder = new StringBuilder(); + private readonly int _indent; + private readonly string _indentStr; + private readonly object? _obj; + + public HumanizeJson(object? obj, int indent) + { + _obj = obj; + _indent = indent; + _indentStr = new string(' ', indent * 4); + + ParseObject(); + } + + public string GetResult() => _obj == null ? string.Empty : _builder.ToString().TrimEnd(); + + private void ParseObject() + { + if (_obj == null) + { + return; + } + + switch (_obj) + { + case Dictionary dictionary: + AppendDictionary(dictionary); + break; + case List list: + AppendList(list); + break; + default: + AppendString(_obj.ToString()); + break; + } + } + + private void AppendDictionary(Dictionary objects) + { + foreach (var kvp in objects) + { + if (kvp.Value == null) continue; + + var writeOutput = false; + + switch (kvp.Value) + { + case Dictionary valueDictionary: + if (valueDictionary.Count > 0) + { + writeOutput = true; + _builder + .Append($"{_indentStr}{kvp.Key,-16}: object") + .AppendLine(); + } + + break; + case List valueList: + if (valueList.Count > 0) + { + writeOutput = true; + _builder + .Append($"{_indentStr}{kvp.Key,-16}: array[{valueList.Count}]") + .AppendLine(); + } + + break; + default: + writeOutput = true; + _builder.Append($"{_indentStr}{kvp.Key,-16}: "); + break; + } + + if (writeOutput) + _builder.AppendLine(new HumanizeJson(kvp.Value, _indent + 1).GetResult()); + } + } + + private void AppendList(IEnumerable objects) + { + var index = 0; + foreach (var value in objects) + { + var writeOutput = false; + + switch (value) + { + case Dictionary valueDictionary: + if (valueDictionary.Count > 0) + { + writeOutput = true; + _builder + .Append($"{_indentStr}[{index}]: object") + .AppendLine(); + } + + break; + case List valueList: + if (valueList.Count > 0) + { + writeOutput = true; + _builder + .Append($"{_indentStr}[{index}]: array[{valueList.Count}]") + .AppendLine(); + } + + break; + default: + writeOutput = true; + _builder.Append($"{_indentStr}[{index}]: "); + break; + } + + index++; + + if (writeOutput) + _builder.AppendLine(new HumanizeJson(value, _indent + 1).GetResult()); + } + } + + private void AppendString(string stringValue) + { + if (stringValue.Length + _indentStr.Length <= 96 && stringValue.IndexOf('\r') < 0 && + stringValue.IndexOf('\n') < 0) + { + _builder.Append($"{stringValue}"); + return; + } + + _builder.AppendLine(); + var stringLines = stringValue.ToLines().Select(l => l.Trim()); + + foreach (var line in stringLines) + { + _builder.AppendLine($"{_indentStr}{line}"); + } + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Formatters/Json.Converter.cs b/Vendor/Swan.Lite-3.1.0/Formatters/Json.Converter.cs new file mode 100644 index 0000000..c7e18b6 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Formatters/Json.Converter.cs @@ -0,0 +1,353 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using Swan.Reflection; + +namespace Swan.Formatters +{ + /// + /// A very simple, light-weight JSON library written by Mario + /// to teach Geo how things are done + /// + /// This is an useful helper for small tasks but it doesn't represent a full-featured + /// serializer such as the beloved Json.NET. + /// + public static partial class Json + { + private class Converter + { + private static readonly ConcurrentDictionary MemberInfoNameCache = + new ConcurrentDictionary(); + + private static readonly ConcurrentDictionary ListAddMethodCache = new ConcurrentDictionary(); + + private readonly object? _target; + private readonly Type _targetType; + private readonly bool _includeNonPublic; + private readonly JsonSerializerCase _jsonSerializerCase; + + private Converter( + object? source, + Type targetType, + ref object? targetInstance, + bool includeNonPublic, + JsonSerializerCase jsonSerializerCase) + { + _targetType = targetInstance != null ? targetInstance.GetType() : targetType; + _includeNonPublic = includeNonPublic; + _jsonSerializerCase = jsonSerializerCase; + + if (source == null) + { + return; + } + + var sourceType = source.GetType(); + + if (_targetType == null || _targetType == typeof(object)) _targetType = sourceType; + if (sourceType == _targetType) + { + _target = source; + return; + } + + if (!TrySetInstance(targetInstance, source, ref _target)) + return; + + ResolveObject(source, ref _target); + } + + internal static object? FromJsonResult( + object? source, + JsonSerializerCase jsonSerializerCase, + Type? targetType = null, + bool includeNonPublic = false) + { + object? nullRef = null; + return new Converter(source, targetType ?? typeof(object), ref nullRef, includeNonPublic, jsonSerializerCase).GetResult(); + } + + private static object? FromJsonResult(object source, + Type targetType, + ref object? targetInstance, + bool includeNonPublic) + { + return new Converter(source, targetType, ref targetInstance, includeNonPublic, JsonSerializerCase.None).GetResult(); + } + + private static Type? GetAddMethodParameterType(Type targetType) + => ListAddMethodCache.GetOrAdd(targetType, + x => x.GetMethods() + .FirstOrDefault( + m => m.Name == AddMethodName && m.IsPublic && m.GetParameters().Length == 1)? + .GetParameters()[0] + .ParameterType); + + private static void GetByteArray(string sourceString, ref object? target) + { + try + { + target = Convert.FromBase64String(sourceString); + } // Try conversion from Base 64 + catch (FormatException) + { + target = Encoding.UTF8.GetBytes(sourceString); + } // Get the string bytes in UTF8 + } + + private object? GetSourcePropertyValue( + IDictionary sourceProperties, + MemberInfo targetProperty) + { + var targetPropertyName = MemberInfoNameCache.GetOrAdd( + targetProperty, + x => AttributeCache.DefaultCache.Value.RetrieveOne(x)?.PropertyName ?? x.Name.GetNameWithCase(_jsonSerializerCase)); + + return sourceProperties!.GetValueOrDefault(targetPropertyName); + } + + private bool TrySetInstance(object? targetInstance, object source, ref object? target) + { + if (targetInstance == null) + { + // Try to create a default instance + try + { + source.CreateTarget(_targetType, _includeNonPublic, ref target); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + return false; + } + } + else + { + target = targetInstance; + } + + return true; + } + + private object? GetResult() => _target ?? _targetType.GetDefault(); + + private void ResolveObject(object source, ref object? target) + { + switch (source) + { + // Case 0: Special Cases Handling (Source and Target are of specific convertible types) + // Case 0.1: Source is string, Target is byte[] + case string sourceString when _targetType == typeof(byte[]): + GetByteArray(sourceString, ref target); + break; + + // Case 1.1: Source is Dictionary, Target is IDictionary + case Dictionary sourceProperties when target is IDictionary targetDictionary: + PopulateDictionary(sourceProperties, targetDictionary); + break; + + // Case 1.2: Source is Dictionary, Target is not IDictionary (i.e. it is a complex type) + case Dictionary sourceProperties: + PopulateObject(sourceProperties); + break; + + // Case 2.1: Source is List, Target is Array + case List sourceList when target is Array targetArray: + PopulateArray(sourceList, targetArray); + break; + + // Case 2.2: Source is List, Target is IList + case List sourceList when target is IList targetList: + PopulateIList(sourceList, targetList); + break; + + // Case 3: Source is a simple type; Attempt conversion + default: + var sourceStringValue = source.ToStringInvariant(); + + // Handle basic types or enumerations if not + if (!_targetType.TryParseBasicType(sourceStringValue, out target)) + GetEnumValue(sourceStringValue, ref target); + + break; + } + } + + private void PopulateIList(IEnumerable objects, IList list) + { + var parameterType = GetAddMethodParameterType(_targetType); + if (parameterType == null) return; + + foreach (var item in objects) + { + try + { + list.Add(FromJsonResult( + item, + _jsonSerializerCase, + parameterType, + _includeNonPublic)); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // ignored + } + } + } + + private void PopulateArray(IList objects, Array array) + { + var elementType = _targetType.GetElementType(); + + for (var i = 0; i < objects.Count; i++) + { + try + { + var targetItem = FromJsonResult( + objects[i], + _jsonSerializerCase, + elementType, + _includeNonPublic); + array.SetValue(targetItem, i); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // ignored + } + } + } + + private void GetEnumValue(string sourceStringValue, ref object? target) + { + var enumType = Nullable.GetUnderlyingType(_targetType); + if (enumType == null && _targetType.IsEnum) enumType = _targetType; + if (enumType == null) return; + + try + { + target = Enum.Parse(enumType, sourceStringValue); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // ignored + } + } + + private void PopulateDictionary(IDictionary sourceProperties, IDictionary targetDictionary) + { + // find the add method of the target dictionary + var addMethod = _targetType.GetMethods() + .FirstOrDefault( + m => m.Name == AddMethodName && m.IsPublic && m.GetParameters().Length == 2); + + // skip if we don't have a compatible add method + if (addMethod == null) return; + var addMethodParameters = addMethod.GetParameters(); + if (addMethodParameters[0].ParameterType != typeof(string)) return; + + // Retrieve the target entry type + var targetEntryType = addMethodParameters[1].ParameterType; + + // Add the items to the target dictionary + foreach (var sourceProperty in sourceProperties) + { + try + { + var targetEntryValue = FromJsonResult( + sourceProperty.Value, + _jsonSerializerCase, + targetEntryType, + _includeNonPublic); + targetDictionary.Add(sourceProperty.Key, targetEntryValue); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // ignored + } + } + } + + private void PopulateObject(IDictionary sourceProperties) + { + if (sourceProperties == null) + return; + + if (_targetType.IsValueType) + PopulateFields(sourceProperties); + + PopulateProperties(sourceProperties); + } + + private void PopulateProperties(IDictionary sourceProperties) + { + var properties = PropertyTypeCache.DefaultCache.Value.RetrieveFilteredProperties(_targetType, false, p => p.CanWrite); + + foreach (var property in properties) + { + var sourcePropertyValue = GetSourcePropertyValue(sourceProperties, property); + if (sourcePropertyValue == null) continue; + + try + { + var currentPropertyValue = !property.PropertyType.IsArray + ? _target.ReadProperty(property.Name) + : null; + + var targetPropertyValue = FromJsonResult( + sourcePropertyValue, + property.PropertyType, + ref currentPropertyValue, + _includeNonPublic); + + _target.WriteProperty(property.Name, targetPropertyValue); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // ignored + } + } + } + + private void PopulateFields(IDictionary sourceProperties) + { + foreach (var field in FieldTypeCache.DefaultCache.Value.RetrieveAllFields(_targetType)) + { + var sourcePropertyValue = GetSourcePropertyValue(sourceProperties, field); + if (sourcePropertyValue == null) continue; + + var targetPropertyValue = FromJsonResult( + sourcePropertyValue, + _jsonSerializerCase, + field.FieldType, + _includeNonPublic); + + try + { + field.SetValue(_target, targetPropertyValue); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // ignored + } + } + } + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Formatters/Json.Deserializer.cs b/Vendor/Swan.Lite-3.1.0/Formatters/Json.Deserializer.cs new file mode 100644 index 0000000..de89a10 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Formatters/Json.Deserializer.cs @@ -0,0 +1,348 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Swan.Formatters +{ + /// + /// A very simple, light-weight JSON library written by Mario + /// to teach Geo how things are done + /// + /// This is an useful helper for small tasks but it doesn't represent a full-featured + /// serializer such as the beloved Json.NET. + /// + public partial class Json + { + /// + /// A simple JSON Deserializer. + /// + private class Deserializer + { + #region State Variables + + private readonly object? _result; + private readonly string _json; + + private Dictionary _resultObject; + private List _resultArray; + private ReadState _state = ReadState.WaitingForRootOpen; + private string? _currentFieldName; + + private int _index; + + #endregion + + private Deserializer(string json, int startIndex) + { + _json = json; + + for (_index = startIndex; _index < _json.Length; _index++) + { + switch (_state) + { + case ReadState.WaitingForRootOpen: + WaitForRootOpen(); + continue; + case ReadState.WaitingForField when char.IsWhiteSpace(_json, _index): + continue; + case ReadState.WaitingForField when (_resultObject != null && _json[_index] == CloseObjectChar) + || (_resultArray != null && _json[_index] == CloseArrayChar): + // Handle empty arrays and empty objects + _result = _resultObject ?? _resultArray as object; + return; + case ReadState.WaitingForField when _json[_index] != StringQuotedChar: + throw CreateParserException($"'{StringQuotedChar}'"); + case ReadState.WaitingForField: + { + var charCount = GetFieldNameCount(); + + _currentFieldName = Unescape(_json.SliceLength(_index + 1, charCount)); + _index += charCount + 1; + _state = ReadState.WaitingForColon; + continue; + } + + case ReadState.WaitingForColon when char.IsWhiteSpace(_json, _index): + continue; + case ReadState.WaitingForColon when _json[_index] != ValueSeparatorChar: + throw CreateParserException($"'{ValueSeparatorChar}'"); + case ReadState.WaitingForColon: + _state = ReadState.WaitingForValue; + continue; + case ReadState.WaitingForValue when char.IsWhiteSpace(_json, _index): + continue; + case ReadState.WaitingForValue when (_resultObject != null && _json[_index] == CloseObjectChar) + || (_resultArray != null && _json[_index] == CloseArrayChar): + // Handle empty arrays and empty objects + _result = _resultObject ?? _resultArray as object; + return; + case ReadState.WaitingForValue: + ExtractValue(); + continue; + } + + if (_state != ReadState.WaitingForNextOrRootClose || char.IsWhiteSpace(_json, _index)) continue; + + if (_json[_index] == FieldSeparatorChar) + { + if (_resultObject != null) + { + _state = ReadState.WaitingForField; + _currentFieldName = null; + continue; + } + + _state = ReadState.WaitingForValue; + continue; + } + + if ((_resultObject == null || _json[_index] != CloseObjectChar) && + (_resultArray == null || _json[_index] != CloseArrayChar)) + { + throw CreateParserException($"'{FieldSeparatorChar}' '{CloseObjectChar}' or '{CloseArrayChar}'"); + } + + _result = _resultObject ?? _resultArray as object; + return; + } + } + + internal static object? DeserializeInternal(string json) => new Deserializer(json, 0)._result; + + private void WaitForRootOpen() + { + if (char.IsWhiteSpace(_json, _index)) return; + + switch (_json[_index]) + { + case OpenObjectChar: + _resultObject = new Dictionary(); + _state = ReadState.WaitingForField; + return; + case OpenArrayChar: + _resultArray = new List(); + _state = ReadState.WaitingForValue; + return; + default: + throw CreateParserException($"'{OpenObjectChar}' or '{OpenArrayChar}'"); + } + } + + private void ExtractValue() + { + // determine the value based on what it starts with + switch (_json[_index]) + { + case StringQuotedChar: // expect a string + ExtractStringQuoted(); + break; + + case OpenObjectChar: // expect object + case OpenArrayChar: // expect array + ExtractObject(); + break; + + case 't': // expect true + ExtractConstant(TrueLiteral, true); + break; + + case 'f': // expect false + ExtractConstant(FalseLiteral, false); + break; + + case 'n': // expect null + ExtractConstant(NullLiteral); + break; + + default: // expect number + ExtractNumber(); + break; + } + + _currentFieldName = null; + _state = ReadState.WaitingForNextOrRootClose; + } + + private static string Unescape(string str) + { + // check if we need to unescape at all + if (str.IndexOf(StringEscapeChar) < 0) + return str; + + var builder = new StringBuilder(str.Length); + for (var i = 0; i < str.Length; i++) + { + if (str[i] != StringEscapeChar) + { + builder.Append(str[i]); + continue; + } + + if (i + 1 > str.Length - 1) + break; + + // escape sequence begins here + switch (str[i + 1]) + { + case 'u': + i = ExtractEscapeSequence(str, i, builder); + break; + case 'b': + builder.Append('\b'); + i += 1; + break; + case 't': + builder.Append('\t'); + i += 1; + break; + case 'n': + builder.Append('\n'); + i += 1; + break; + case 'f': + builder.Append('\f'); + i += 1; + break; + case 'r': + builder.Append('\r'); + i += 1; + break; + default: + builder.Append(str[i + 1]); + i += 1; + break; + } + } + + return builder.ToString(); + } + + private static int ExtractEscapeSequence(string str, int i, StringBuilder builder) + { + var startIndex = i + 2; + var endIndex = i + 5; + if (endIndex > str.Length - 1) + { + builder.Append(str[i + 1]); + i += 1; + return i; + } + + var hexCode = str.Slice(startIndex, endIndex).ConvertHexadecimalToBytes(); + builder.Append(Encoding.BigEndianUnicode.GetChars(hexCode)); + i += 5; + return i; + } + + private int GetFieldNameCount() + { + var charCount = 0; + for (var j = _index + 1; j < _json.Length; j++) + { + if (_json[j] == StringQuotedChar && _json[j - 1] != StringEscapeChar) + break; + + charCount++; + } + + return charCount; + } + + private void ExtractObject() + { + // Extract and set the value + var deserializer = new Deserializer(_json, _index); + + if (_currentFieldName != null) + _resultObject[_currentFieldName] = deserializer._result; + else + _resultArray.Add(deserializer._result); + + _index = deserializer._index; + } + + private void ExtractNumber() + { + var charCount = 0; + for (var j = _index; j < _json.Length; j++) + { + if (char.IsWhiteSpace(_json[j]) || _json[j] == FieldSeparatorChar + || (_resultObject != null && _json[j] == CloseObjectChar) + || (_resultArray != null && _json[j] == CloseArrayChar)) + break; + + charCount++; + } + + // Extract and set the value + var stringValue = _json.SliceLength(_index, charCount); + + if (decimal.TryParse(stringValue, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.InvariantCulture, out var value) == false) + throw CreateParserException("[number]"); + + if (_currentFieldName != null) + _resultObject[_currentFieldName] = value; + else + _resultArray.Add(value); + + _index += charCount - 1; + } + + private void ExtractConstant(string boolValue, bool? value = null) + { + if (_json.SliceLength(_index, boolValue.Length) != boolValue) + throw CreateParserException($"'{ValueSeparatorChar}'"); + + // Extract and set the value + if (_currentFieldName != null) + _resultObject[_currentFieldName] = value; + else + _resultArray.Add(value); + + _index += boolValue.Length - 1; + } + + private void ExtractStringQuoted() + { + var charCount = 0; + var escapeCharFound = false; + for (var j = _index + 1; j < _json.Length; j++) + { + if (_json[j] == StringQuotedChar && !escapeCharFound) + break; + + escapeCharFound = _json[j] == StringEscapeChar && !escapeCharFound; + charCount++; + } + + // Extract and set the value + var value = Unescape(_json.SliceLength(_index + 1, charCount)); + if (_currentFieldName != null) + _resultObject[_currentFieldName] = value; + else + _resultArray.Add(value); + + _index += charCount + 1; + } + + private FormatException CreateParserException(string expected) + { + var (line, col) = _json.TextPositionAt(_index); + return new FormatException( + $"Parser error (Line {line}, Col {col}, State {_state}): Expected {expected} but got '{_json[_index]}'."); + } + + /// + /// Defines the different JSON read states. + /// + private enum ReadState + { + WaitingForRootOpen, + WaitingForField, + WaitingForColon, + WaitingForValue, + WaitingForNextOrRootClose, + } + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Formatters/Json.Serializer.cs b/Vendor/Swan.Lite-3.1.0/Formatters/Json.Serializer.cs new file mode 100644 index 0000000..ff3a0c5 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Formatters/Json.Serializer.cs @@ -0,0 +1,370 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using Swan.Reflection; + +namespace Swan.Formatters +{ + /// + /// A very simple, light-weight JSON library written by Mario + /// to teach Geo how things are done + /// + /// This is an useful helper for small tasks but it doesn't represent a full-featured + /// serializer such as the beloved Json.NET. + /// + public partial class Json + { + /// + /// A simple JSON serializer. + /// + private class Serializer + { + #region Private Declarations + + private static readonly Dictionary IndentStrings = new Dictionary(); + + private readonly SerializerOptions _options; + private readonly string _result; + private readonly StringBuilder _builder; + private readonly string _lastCommaSearch; + private readonly string[]? _excludedNames = null; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The object. + /// The depth. + /// The options. + private Serializer(object? obj, int depth, SerializerOptions options, string[]? excludedNames = null) + { + if (depth > 20) + { + throw new InvalidOperationException( + "The max depth (20) has been reached. Serializer can not continue."); + } + + // Basic Type Handling (nulls, strings, number, date and bool) + _result = ResolveBasicType(obj); + + if (!string.IsNullOrWhiteSpace(_result)) + return; + + _options = options; + _excludedNames ??= excludedNames; + _options.ExcludeProperties = GetExcludedNames(obj?.GetType(), _excludedNames); + + // Handle circular references correctly and avoid them + if (options.IsObjectPresent(obj!)) + { + _result = $"{{ \"$circref\": \"{Escape(obj!.GetHashCode().ToStringInvariant(), false)}\" }}"; + return; + } + + // At this point, we will need to construct the object with a StringBuilder. + _lastCommaSearch = FieldSeparatorChar + (_options.Format ? Environment.NewLine : string.Empty); + _builder = new StringBuilder(); + + _result = obj switch + { + IDictionary itemsZero when itemsZero.Count == 0 => EmptyObjectLiteral, + IDictionary items => ResolveDictionary(items, depth), + IEnumerable enumerableZero when !enumerableZero.Cast().Any() => EmptyArrayLiteral, + IEnumerable enumerableBytes when enumerableBytes is byte[] bytes => Serialize(bytes.ToBase64(), depth, _options, _excludedNames), + IEnumerable enumerable => ResolveEnumerable(enumerable, depth), + _ => ResolveObject(obj!, depth) + }; + } + + internal static string Serialize(object? obj, int depth, SerializerOptions options, string[]? excludedNames = null) => new Serializer(obj, depth, options, excludedNames)._result; + + #endregion + + #region Helper Methods + internal static string[]? GetExcludedNames(Type? type, string[]? excludedNames) + { + if (type == null) + return excludedNames; + + var excludedByAttr = IgnoredPropertiesCache.Retrieve(type, t => t.GetProperties() + .Where(x => AttributeCache.DefaultCache.Value.RetrieveOne(x)?.Ignored == true) + .Select(x => x.Name)); + + if (excludedByAttr?.Any() != true) + return excludedNames; + + return excludedNames?.Any(string.IsNullOrWhiteSpace) == true + ? excludedByAttr.Intersect(excludedNames.Where(y => !string.IsNullOrWhiteSpace(y))).ToArray() + : excludedByAttr.ToArray(); + } + + private static string ResolveBasicType(object? obj) + { + switch (obj) + { + case null: + return NullLiteral; + case string s: + return Escape(s, true); + case bool b: + return b ? TrueLiteral : FalseLiteral; + case Type _: + case Assembly _: + case MethodInfo _: + case PropertyInfo _: + case EventInfo _: + return Escape(obj.ToString(), true); + case DateTime d: + return $"{StringQuotedChar}{d:s}{StringQuotedChar}"; + default: + var targetType = obj.GetType(); + + if (!Definitions.BasicTypesInfo.Value.ContainsKey(targetType)) + return string.Empty; + + var escapedValue = Escape(Definitions.BasicTypesInfo.Value[targetType].ToStringInvariant(obj), false); + + return decimal.TryParse(escapedValue, out _) + ? $"{escapedValue}" + : $"{StringQuotedChar}{escapedValue}{StringQuotedChar}"; + } + } + + private static bool IsNonEmptyJsonArrayOrObject(string serialized) + { + if (serialized == EmptyObjectLiteral || serialized == EmptyArrayLiteral) return false; + + // find the first position the character is not a space + return serialized.Where(c => c != ' ').Select(c => c == OpenObjectChar || c == OpenArrayChar).FirstOrDefault(); + } + + private static string Escape(string str, bool quoted) + { + if (str == null) + return string.Empty; + + var builder = new StringBuilder(str.Length * 2); + if (quoted) builder.Append(StringQuotedChar); + Escape(str, builder); + if (quoted) builder.Append(StringQuotedChar); + return builder.ToString(); + } + + private static void Escape(string str, StringBuilder builder) + { + foreach (var currentChar in str) + { + switch (currentChar) + { + case '\\': + case '"': + case '/': + builder + .Append('\\') + .Append(currentChar); + break; + case '\b': + builder.Append("\\b"); + break; + case '\t': + builder.Append("\\t"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\r': + builder.Append("\\r"); + break; + default: + if (currentChar < ' ') + { + var escapeBytes = BitConverter.GetBytes((ushort)currentChar); + if (BitConverter.IsLittleEndian == false) + Array.Reverse(escapeBytes); + + builder.Append("\\u") + .Append(escapeBytes[1].ToString("X", CultureInfo.InvariantCulture).PadLeft(2, '0')) + .Append(escapeBytes[0].ToString("X", CultureInfo.InvariantCulture).PadLeft(2, '0')); + } + else + { + builder.Append(currentChar); + } + + break; + } + } + } + + private Dictionary CreateDictionary( + Dictionary fields, + string targetType, + object target) + { + // Create the dictionary and extract the properties + var objectDictionary = new Dictionary(); + + if (!string.IsNullOrWhiteSpace(_options.TypeSpecifier)) + objectDictionary[_options.TypeSpecifier!] = targetType; + + foreach (var field in fields) + { + // Build the dictionary using property names and values + // Note: used to be: property.GetValue(target); but we would be reading private properties + try + { + objectDictionary[field.Key] = field.Value is PropertyInfo property + ? target.ReadProperty(property.Name) + : (field.Value as FieldInfo)?.GetValue(target); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + /* ignored */ + } + } + + return objectDictionary; + } + + private string ResolveDictionary(IDictionary items, int depth) + { + Append(OpenObjectChar, depth); + AppendLine(); + + // Iterate through the elements and output recursively + var writeCount = 0; + foreach (var key in items.Keys) + { + // Serialize and append the key (first char indented) + Append(StringQuotedChar, depth + 1); + Escape(key.ToString(), _builder); + _builder + .Append(StringQuotedChar) + .Append(ValueSeparatorChar) + .Append(" "); + + // Serialize and append the value + var serializedValue = Serialize(items[key], depth + 1, _options, _excludedNames); + + if (IsNonEmptyJsonArrayOrObject(serializedValue)) AppendLine(); + Append(serializedValue, 0); + + // Add a comma and start a new line -- We will remove the last one when we are done writing the elements + Append(FieldSeparatorChar, 0); + AppendLine(); + writeCount++; + } + + // Output the end of the object and set the result + RemoveLastComma(); + Append(CloseObjectChar, writeCount > 0 ? depth : 0); + return _builder.ToString(); + } + + private string ResolveObject(object target, int depth) + { + var targetType = target.GetType(); + + if (targetType.IsEnum) + return Convert.ToInt64(target, CultureInfo.InvariantCulture).ToStringInvariant(); + + var fields = _options.GetProperties(targetType); + + if (fields.Count == 0 && string.IsNullOrWhiteSpace(_options.TypeSpecifier)) + return EmptyObjectLiteral; + + // If we arrive here, then we convert the object into a + // dictionary of property names and values and call the serialization + // function again + var objectDictionary = CreateDictionary(fields, targetType.ToString(), target); + + return Serialize(objectDictionary, depth, _options, _excludedNames); + } + + private string ResolveEnumerable(IEnumerable target, int depth) + { + // Cast the items as a generic object array + var items = target.Cast(); + + Append(OpenArrayChar, depth); + AppendLine(); + + // Iterate through the elements and output recursively + var writeCount = 0; + foreach (var entry in items) + { + var serializedValue = Serialize(entry, depth + 1, _options, _excludedNames); + + if (IsNonEmptyJsonArrayOrObject(serializedValue)) + Append(serializedValue, 0); + else + Append(serializedValue, depth + 1); + + Append(FieldSeparatorChar, 0); + AppendLine(); + writeCount++; + } + + // Output the end of the array and set the result + RemoveLastComma(); + Append(CloseArrayChar, writeCount > 0 ? depth : 0); + return _builder.ToString(); + } + + private void SetIndent(int depth) + { + if (_options.Format == false || depth <= 0) return; + + _builder.Append(IndentStrings.GetOrAdd(depth, x => new string(' ', x * 4))); + } + + /// + /// Removes the last comma in the current string builder. + /// + private void RemoveLastComma() + { + if (_builder.Length < _lastCommaSearch.Length) + return; + + if (_lastCommaSearch.Where((t, i) => _builder[_builder.Length - _lastCommaSearch.Length + i] != t).Any()) + { + return; + } + + // If we got this far, we simply remove the comma character + _builder.Remove(_builder.Length - _lastCommaSearch.Length, 1); + } + + private void Append(string text, int depth) + { + SetIndent(depth); + _builder.Append(text); + } + + private void Append(char text, int depth) + { + SetIndent(depth); + _builder.Append(text); + } + + private void AppendLine() + { + if (_options.Format == false) return; + _builder.Append(Environment.NewLine); + } + #endregion + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Formatters/Json.SerializerOptions.cs b/Vendor/Swan.Lite-3.1.0/Formatters/Json.SerializerOptions.cs new file mode 100644 index 0000000..8bae789 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Formatters/Json.SerializerOptions.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Swan.Reflection; + +namespace Swan.Formatters +{ + /// + /// A very simple, light-weight JSON library written by Mario + /// to teach Geo how things are done + /// + /// This is an useful helper for small tasks but it doesn't represent a full-featured + /// serializer such as the beloved Json.NET. + /// + public class SerializerOptions + { + private static readonly ConcurrentDictionary, MemberInfo>> + TypeCache = new ConcurrentDictionary, MemberInfo>>(); + + private readonly string[]? _includeProperties; + private readonly Dictionary> _parentReferences = new Dictionary>(); + + /// + /// Initializes a new instance of the class. + /// + /// if set to true [format]. + /// The type specifier. + /// The include properties. + /// The exclude properties. + /// if set to true [include non public]. + /// The parent references. + /// The json serializer case. + public SerializerOptions( + bool format, + string? typeSpecifier, + string[]? includeProperties, + string[]? excludeProperties = null, + bool includeNonPublic = true, + IReadOnlyCollection? parentReferences = null, + JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None) + { + _includeProperties = includeProperties; + + ExcludeProperties = excludeProperties; + IncludeNonPublic = includeNonPublic; + Format = format; + TypeSpecifier = typeSpecifier; + JsonSerializerCase = jsonSerializerCase; + + if (parentReferences == null) + return; + + foreach (var parentReference in parentReferences.Where(x => x.IsAlive)) + { + IsObjectPresent(parentReference.Target); + } + } + + /// + /// Gets a value indicating whether this is format. + /// + /// + /// true if format; otherwise, false. + /// + public bool Format { get; } + + /// + /// Gets the type specifier. + /// + /// + /// The type specifier. + /// + public string? TypeSpecifier { get; } + + /// + /// Gets a value indicating whether [include non public]. + /// + /// + /// true if [include non public]; otherwise, false. + /// + public bool IncludeNonPublic { get; } + + /// + /// Gets the json serializer case. + /// + /// + /// The json serializer case. + /// + public JsonSerializerCase JsonSerializerCase { get; } + + /// + /// Gets or sets the exclude properties. + /// + /// + /// The exclude properties. + /// + public string[]? ExcludeProperties { get; set; } + + internal bool IsObjectPresent(object target) + { + var hashCode = target.GetHashCode(); + + if (_parentReferences.ContainsKey(hashCode)) + { + if (_parentReferences[hashCode].Any(p => ReferenceEquals(p.Target, target))) + return true; + + _parentReferences[hashCode].Add(new WeakReference(target)); + return false; + } + + _parentReferences.Add(hashCode, new List { new WeakReference(target) }); + return false; + } + + internal Dictionary GetProperties(Type targetType) + => GetPropertiesCache(targetType) + .When(() => _includeProperties?.Length > 0, + query => query.Where(p => _includeProperties.Contains(p.Key.Item1))) + .When(() => ExcludeProperties?.Length > 0, + query => query.Where(p => !ExcludeProperties.Contains(p.Key.Item1))) + .ToDictionary(x => x.Key.Item2, x => x.Value); + + private Dictionary, MemberInfo> GetPropertiesCache(Type targetType) + { + if (TypeCache.TryGetValue(targetType, out var current)) + return current; + + var fields = + new List(PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(targetType).Where(p => p.CanRead)); + + // If the target is a struct (value type) navigate the fields. + if (targetType.IsValueType) + { + fields.AddRange(FieldTypeCache.DefaultCache.Value.RetrieveAllFields(targetType)); + } + + var value = fields + .Where(x => x.GetCustomAttribute()?.Ignored != true) + .ToDictionary( + x => Tuple.Create(x.Name, + x.GetCustomAttribute()?.PropertyName ?? x.Name.GetNameWithCase(JsonSerializerCase)), + x => x); + + TypeCache.TryAdd(targetType, value); + + return value; + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Formatters/Json.cs b/Vendor/Swan.Lite-3.1.0/Formatters/Json.cs new file mode 100644 index 0000000..0b0f373 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Formatters/Json.cs @@ -0,0 +1,373 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Swan.Collections; + +namespace Swan.Formatters +{ + /// + /// A very simple, light-weight JSON library written by Mario + /// to teach Geo how things are done + /// + /// This is an useful helper for small tasks but it doesn't represent a full-featured + /// serializer such as the beloved Json.NET. + /// + public static partial class Json + { + #region Constants + + internal const string AddMethodName = "Add"; + + private const char OpenObjectChar = '{'; + private const char CloseObjectChar = '}'; + + private const char OpenArrayChar = '['; + private const char CloseArrayChar = ']'; + + private const char FieldSeparatorChar = ','; + private const char ValueSeparatorChar = ':'; + + private const char StringEscapeChar = '\\'; + private const char StringQuotedChar = '"'; + + private const string EmptyObjectLiteral = "{ }"; + private const string EmptyArrayLiteral = "[ ]"; + private const string TrueLiteral = "true"; + private const string FalseLiteral = "false"; + private const string NullLiteral = "null"; + + #endregion + + private static readonly CollectionCacheRepository IgnoredPropertiesCache = new CollectionCacheRepository(); + + #region Public API + + /// + /// Serializes the specified object into a JSON string. + /// + /// The object. + /// if set to true it formats and indents the output. + /// The type specifier. Leave null or empty to avoid setting. + /// if set to true non-public getters will be also read. + /// The included property names. + /// The excluded property names. + /// + /// A that represents the current object. + /// + /// + /// The following example describes how to serialize a simple object. + /// + /// using Swan.Formatters; + /// + /// class Example + /// { + /// static void Main() + /// { + /// var obj = new { One = "One", Two = "Two" }; + /// + /// var serial = Json.Serialize(obj); // {"One": "One","Two": "Two"} + /// } + /// } + /// + /// + /// The following example details how to serialize an object using the . + /// + /// + /// using Swan.Attributes; + /// using Swan.Formatters; + /// + /// class Example + /// { + /// class JsonPropertyExample + /// { + /// [JsonProperty("data")] + /// public string Data { get; set; } + /// + /// [JsonProperty("ignoredData", true)] + /// public string IgnoredData { get; set; } + /// } + /// + /// static void Main() + /// { + /// var obj = new JsonPropertyExample() { Data = "OK", IgnoredData = "OK" }; + /// + /// // {"data": "OK"} + /// var serializedObj = Json.Serialize(obj); + /// } + /// } + /// + /// + public static string Serialize( + object? obj, + bool format = false, + string? typeSpecifier = null, + bool includeNonPublic = false, + string[]? includedNames = null, + params string[] excludedNames) => + Serialize(obj, format, typeSpecifier, includeNonPublic, includedNames, excludedNames, null, JsonSerializerCase.None); + + /// + /// Serializes the specified object into a JSON string. + /// + /// The object. + /// The json serializer case. + /// if set to true [format]. + /// The type specifier. + /// + /// A that represents the current object. + /// + public static string Serialize( + object? obj, + JsonSerializerCase jsonSerializerCase, + bool format = false, + string? typeSpecifier = null) => Serialize(obj, format, typeSpecifier, false, null, null, null, jsonSerializerCase); + + /// + /// Serializes the specified object into a JSON string. + /// + /// The object. + /// if set to true it formats and indents the output. + /// The type specifier. Leave null or empty to avoid setting. + /// if set to true non-public getters will be also read. + /// The included property names. + /// The excluded property names. + /// The parent references. + /// The json serializer case. + /// + /// A that represents the current object. + /// + public static string Serialize( + object? obj, + bool format, + string? typeSpecifier, + bool includeNonPublic, + string[]? includedNames, + string[]? excludedNames, + List? parentReferences, + JsonSerializerCase jsonSerializerCase) + { + if (obj != null && (obj is string || Definitions.AllBasicValueTypes.Contains(obj.GetType()))) + { + return SerializePrimitiveValue(obj); + } + + var options = new SerializerOptions( + format, + typeSpecifier, + includedNames, + Serializer.GetExcludedNames(obj?.GetType(), excludedNames), + includeNonPublic, + parentReferences, + jsonSerializerCase); + + return Serializer.Serialize(obj, 0, options, excludedNames); + } + + /// + /// Serializes the specified object using the SerializerOptions provided. + /// + /// The object. + /// The options. + /// + /// A that represents the current object. + /// + public static string Serialize(object? obj, SerializerOptions options) => Serializer.Serialize(obj, 0, options); + + /// + /// Serializes the specified object only including the specified property names. + /// + /// The object. + /// if set to true it formats and indents the output. + /// The include names. + /// A that represents the current object. + /// + /// The following example shows how to serialize a simple object including the specified properties. + /// + /// using Swan.Formatters; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // object to serialize + /// var obj = new { One = "One", Two = "Two", Three = "Three" }; + /// + /// // the included names + /// var includedNames = new[] { "Two", "Three" }; + /// + /// // serialize only the included names + /// var data = Json.SerializeOnly(basicObject, true, includedNames); + /// // {"Two": "Two","Three": "Three" } + /// } + /// } + /// + /// + public static string SerializeOnly(object? obj, bool format, params string[] includeNames) + => Serialize(obj, new SerializerOptions(format, null, includeNames)); + + /// + /// Serializes the specified object excluding the specified property names. + /// + /// The object. + /// if set to true it formats and indents the output. + /// The exclude names. + /// A that represents the current object. + /// + /// The following code shows how to serialize a simple object excluding the specified properties. + /// + /// using Swan.Formatters; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // object to serialize + /// var obj = new { One = "One", Two = "Two", Three = "Three" }; + /// + /// // the excluded names + /// var excludeNames = new[] { "Two", "Three" }; + /// + /// // serialize excluding + /// var data = Json.SerializeExcluding(basicObject, false, includedNames); + /// // {"One": "One"} + /// } + /// } + /// + /// + public static string SerializeExcluding(object? obj, bool format, params string[] excludeNames) + => Serializer.Serialize(obj, 0, new SerializerOptions(format, null, null), excludeNames); + + /// + /// Deserializes the specified json string as either a Dictionary[string, object] or as a List[object] + /// depending on the syntax of the JSON string. + /// + /// The JSON string. + /// The json serializer case. + /// + /// Type of the current deserializes. + /// + /// + /// The following code shows how to deserialize a JSON string into a Dictionary. + /// + /// using Swan.Formatters; + /// class Example + /// { + /// static void Main() + /// { + /// // json to deserialize + /// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}"; + /// // deserializes the specified json into a Dictionary<string, object>. + /// var data = Json.Deserialize(basicJson, JsonSerializerCase.None); + /// } + /// } + /// + public static object? Deserialize(string json, JsonSerializerCase jsonSerializerCase) => + json == null + ? throw new ArgumentNullException(nameof(json)) + : Converter.FromJsonResult(Deserializer.DeserializeInternal(json), jsonSerializerCase); + + /// + /// Deserializes the specified json string as either a Dictionary[string, object] or as a List[object] + /// depending on the syntax of the JSON string. + /// + /// The JSON string. + /// + /// Type of the current deserializes. + /// + /// + /// The following code shows how to deserialize a JSON string into a Dictionary. + /// + /// using Swan.Formatters; + /// class Example + /// { + /// static void Main() + /// { + /// // json to deserialize + /// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}"; + /// // deserializes the specified json into a Dictionary<string, object>. + /// var data = Json.Deserialize(basicJson); + /// } + /// } + /// + public static object? Deserialize(string json) => + json == null + ? throw new ArgumentNullException(nameof(json)) + : Deserialize(json, JsonSerializerCase.None); + + /// + /// Deserializes the specified JSON string and converts it to the specified object type. + /// Non-public constructors and property setters are ignored. + /// + /// The type of object to deserialize. + /// The JSON string. + /// The JSON serializer case. + /// + /// The deserialized specified type object. + /// + /// + /// The following code describes how to deserialize a JSON string into an object of type T. + /// + /// using Swan.Formatters; + /// class Example + /// { + /// static void Main() + /// { + /// // json type BasicJson to serialize + /// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}"; + /// // deserializes the specified string in a new instance of the type BasicJson. + /// var data = Json.Deserialize<BasicJson>(basicJson); + /// } + /// } + /// + public static T Deserialize(string json, JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None) => + json == null + ? throw new ArgumentNullException(nameof(json)) + : (T)Deserialize(json, typeof(T), jsonSerializerCase: jsonSerializerCase); + + /// + /// Deserializes the specified JSON string and converts it to the specified object type. + /// + /// The type of object to deserialize. + /// The JSON string. + /// if set to true, it also uses the non-public constructors and property setters. + /// + /// The deserialized specified type object. + /// + public static T Deserialize(string json, bool includeNonPublic) => + json == null + ? throw new ArgumentNullException(nameof(json)) + : (T)Deserialize(json, typeof(T), includeNonPublic); + + /// + /// Deserializes the specified JSON string and converts it to the specified object type. + /// + /// The JSON string. + /// Type of the result. + /// if set to true, it also uses the non-public constructors and property setters. + /// The json serializer case. + /// + /// Type of the current conversion from json result. + /// + public static object? Deserialize(string json, Type resultType, bool includeNonPublic = false, JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None) => + json == null + ? throw new ArgumentNullException(nameof(json)) + : Converter.FromJsonResult( + Deserializer.DeserializeInternal(json), + jsonSerializerCase, + resultType, + includeNonPublic); + + #endregion + + #region Private API + private static string SerializePrimitiveValue(object obj) => + obj switch + { + string stringValue => $"\"{stringValue}\"", + bool boolValue => boolValue ? TrueLiteral : FalseLiteral, + _ => obj.ToString() + }; + + #endregion + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Formatters/JsonPropertyAttribute.cs b/Vendor/Swan.Lite-3.1.0/Formatters/JsonPropertyAttribute.cs new file mode 100644 index 0000000..d05ac4d --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Formatters/JsonPropertyAttribute.cs @@ -0,0 +1,39 @@ +using System; + +namespace Swan.Formatters +{ + /// + /// An attribute used to help setup a property behavior when serialize/deserialize JSON. + /// + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class JsonPropertyAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// Name of the property. + /// if set to true [ignored]. + public JsonPropertyAttribute(string propertyName, bool ignored = false) + { + PropertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName)); + Ignored = ignored; + } + + /// + /// Gets or sets the name of the property. + /// + /// + /// The name of the property. + /// + public string PropertyName { get; } + + /// + /// Gets or sets a value indicating whether this is ignored. + /// + /// + /// true if ignored; otherwise, false. + /// + public bool Ignored { get; } + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Formatters/JsonSerializerCase.cs b/Vendor/Swan.Lite-3.1.0/Formatters/JsonSerializerCase.cs new file mode 100644 index 0000000..d525bd4 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Formatters/JsonSerializerCase.cs @@ -0,0 +1,23 @@ +namespace Swan.Formatters +{ + /// + /// Enumerates the JSON serializer cases to use: None (keeps the same case), PascalCase, or camelCase. + /// + public enum JsonSerializerCase + { + /// + /// The none + /// + None, + + /// + /// The pascal case (eg. PascalCase) + /// + PascalCase, + + /// + /// The camel case (eg. camelCase) + /// + CamelCase, + } +} diff --git a/Vendor/Swan.Lite-3.1.0/FromString.cs b/Vendor/Swan.Lite-3.1.0/FromString.cs new file mode 100644 index 0000000..b0de440 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/FromString.cs @@ -0,0 +1,347 @@ +using System; +using System.Collections.Concurrent; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; + +namespace Swan +{ + /// + /// Provides a standard way to convert strings to different types. + /// + public static class FromString + { + // It doesn't matter which converter we get here: ConvertFromInvariantString is not virtual. + private static readonly MethodInfo ConvertFromInvariantStringMethod + = new Func(TypeDescriptor.GetConverter(typeof(int)).ConvertFromInvariantString).Method; + + private static readonly MethodInfo TryConvertToInternalMethod + = typeof(FromString).GetMethod(nameof(TryConvertToInternal), BindingFlags.Static | BindingFlags.NonPublic); + + private static readonly MethodInfo ConvertToInternalMethod + = typeof(FromString).GetMethod(nameof(ConvertToInternal), BindingFlags.Static | BindingFlags.NonPublic); + + private static readonly ConcurrentDictionary> GenericTryConvertToMethods + = new ConcurrentDictionary>(); + + private static readonly ConcurrentDictionary> GenericConvertToMethods + = new ConcurrentDictionary>(); + + /// + /// Determines whether a string can be converted to the specified type. + /// + /// The type resulting from the conversion. + /// if the conversion is possible; + /// otherwise, . + /// is . + public static bool CanConvertTo(Type type) + => TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string)); + + /// + /// Determines whether a string can be converted to the specified type. + /// + /// The type resulting from the conversion. + /// if the conversion is possible; + /// otherwise, . + public static bool CanConvertTo() + => TypeDescriptor.GetConverter(typeof(TResult)).CanConvertFrom(typeof(string)); + + /// + /// Attempts to convert a string to the specified type. + /// + /// The type resulting from the conversion. + /// The string to convert. + /// When this method returns , + /// the result of the conversion. This parameter is passed uninitialized. + /// if the conversion is successful; + /// otherwise, . + /// is . + public static bool TryConvertTo(Type type, string str, out object? result) + { + var converter = TypeDescriptor.GetConverter(type); + if (!converter.CanConvertFrom(typeof(string))) + { + result = null; + return false; + } + + try + { + result = converter.ConvertFromInvariantString(str); + return true; + } + catch (Exception e) when (!e.IsCriticalException()) + { + result = null; + return false; + } + } + + /// + /// Attempts to convert a string to the specified type. + /// + /// The type resulting from the conversion. + /// The string to convert. + /// When this method returns , + /// the result of the conversion. This parameter is passed uninitialized. + /// if the conversion is successful; + /// otherwise, . + public static bool TryConvertTo(string str, out TResult result) + { + var converter = TypeDescriptor.GetConverter(typeof(TResult)); + if (!converter.CanConvertFrom(typeof(string))) + { + result = default; + return false; + } + + try + { + result = (TResult)converter.ConvertFromInvariantString(str); + return true; + } + catch (Exception e) when (!e.IsCriticalException()) + { + result = default; + return false; + } + } + + /// + /// Converts a string to the specified type. + /// + /// The type resulting from the conversion. + /// The string to convert. + /// An instance of . + /// is . + /// The conversion was not successful. + public static object ConvertTo(Type type, string str) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + try + { + return TypeDescriptor.GetConverter(type).ConvertFromInvariantString(str); + } + catch (Exception e) when (!e.IsCriticalException()) + { + throw new StringConversionException(type, e); + } + } + + /// + /// Converts a string to the specified type. + /// + /// The type resulting from the conversion. + /// The string to convert. + /// An instance of . + /// + /// The conversion was not successful. + /// + public static TResult ConvertTo(string str) + { + try + { + return (TResult)TypeDescriptor.GetConverter(typeof(TResult)).ConvertFromInvariantString(str); + } + catch (Exception e) when (!e.IsCriticalException()) + { + throw new StringConversionException(typeof(TResult), e); + } + } + + /// + /// Attempts to convert an array of strings to an array of the specified type. + /// + /// The type resulting from the conversion of each + /// element of . + /// The array to convert. + /// When this method returns , + /// the result of the conversion. This parameter is passed uninitialized. + /// if the conversion is successful; + /// otherwise, . + /// is . + public static bool TryConvertTo(Type type, string[] strings, out object? result) + { + if (strings == null) + { + result = null; + return false; + } + + var method = GenericTryConvertToMethods.GetOrAdd(type, BuildNonGenericTryConvertLambda); + var (success, methodResult) = method(strings); + result = methodResult; + return success; + } + + /// + /// Attempts to convert an array of strings to an array of the specified type. + /// + /// The type resulting from the conversion of each + /// element of . + /// The array to convert. + /// When this method returns , + /// the result of the conversion. This parameter is passed uninitialized. + /// if the conversion is successful; + /// otherwise, . + public static bool TryConvertTo(string[] strings, out TResult[]? result) + { + if (strings == null) + { + result = null; + return false; + } + + var converter = TypeDescriptor.GetConverter(typeof(TResult)); + if (!converter.CanConvertFrom(typeof(string))) + { + result = null; + return false; + } + + try + { + result = new TResult[strings.Length]; + var i = 0; + foreach (var str in strings) + result[i++] = (TResult)converter.ConvertFromInvariantString(str); + + return true; + } + catch (Exception e) when (!e.IsCriticalException()) + { + result = null; + return false; + } + } + + /// + /// Converts an array of strings to an array of the specified type. + /// + /// The type resulting from the conversion of each + /// element of . + /// The array to convert. + /// An array of . + /// is . + /// The conversion of at least one + /// of the elements of was not successful. + public static object? ConvertTo(Type type, string[] strings) + { + if (strings == null) + return null; + + var method = GenericConvertToMethods.GetOrAdd(type, BuildNonGenericConvertLambda); + return method(strings); + } + + /// + /// Converts an array of strings to an array of the specified type. + /// + /// The type resulting from the conversion of each + /// element of . + /// The array to convert. + /// An array of . + /// The conversion of at least one + /// of the elements of was not successful. + public static TResult[]? ConvertTo(string[] strings) + { + if (strings == null) + return null; + + var converter = TypeDescriptor.GetConverter(typeof(TResult)); + var result = new TResult[strings.Length]; + var i = 0; + try + { + foreach (var str in strings) + result[i++] = (TResult)converter.ConvertFromInvariantString(str); + } + catch (Exception e) when (!e.IsCriticalException()) + { + throw new StringConversionException(typeof(TResult), e); + } + + return result; + } + + /// + /// Converts a expression, if the type can be converted to string, to a new expression including + /// the conversion to string. + /// + /// The type. + /// The string. + /// A new expression where the previous expression is converted to string. + public static Expression? ConvertExpressionTo(Type type, Expression str) + { + var converter = TypeDescriptor.GetConverter(type); + + return converter.CanConvertFrom(typeof(string)) + ? Expression.Convert( + Expression.Call(Expression.Constant(converter), ConvertFromInvariantStringMethod, str), + type) + : null; + } + + private static Func BuildNonGenericTryConvertLambda(Type type) + { + var methodInfo = TryConvertToInternalMethod.MakeGenericMethod(type); + var parameter = Expression.Parameter(typeof(string[])); + var body = Expression.Call(methodInfo, parameter); + var lambda = Expression.Lambda>(body, parameter); + return lambda.Compile(); + } + + private static (bool Success, object? Result) TryConvertToInternal(string[] strings) + { + var converter = TypeDescriptor.GetConverter(typeof(TResult)); + if (!converter.CanConvertFrom(typeof(string))) + return (false, null); + + var result = new TResult[strings.Length]; + var i = 0; + + try + { + foreach (var str in strings) + result[i++] = (TResult)converter.ConvertFromInvariantString(str); + + return (true, result); + } + catch (Exception e) when (!e.IsCriticalException()) + { + return (false, null); + } + } + + private static Func BuildNonGenericConvertLambda(Type type) + { + var methodInfo = ConvertToInternalMethod.MakeGenericMethod(type); + var parameter = Expression.Parameter(typeof(string[])); + var body = Expression.Call(methodInfo, parameter); + var lambda = Expression.Lambda>(body, parameter); + + return lambda.Compile(); + } + + private static object ConvertToInternal(string[] strings) + { + var converter = TypeDescriptor.GetConverter(typeof(TResult)); + var result = new TResult[strings.Length]; + var i = 0; + + try + { + foreach (var str in strings) + result[i++] = (TResult)converter.ConvertFromInvariantString(str); + + return result; + } + catch (Exception e) when (!e.IsCriticalException()) + { + throw new StringConversionException(typeof(TResult), e); + } + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/InternalErrorException.cs b/Vendor/Swan.Lite-3.1.0/InternalErrorException.cs new file mode 100644 index 0000000..7884e7e --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/InternalErrorException.cs @@ -0,0 +1,46 @@ +using System; +using System.Runtime.Serialization; + +/* + * NOTE TO CONTRIBUTORS: + * + * Never use this exception directly. + * Use the methods in the SelfCheck class instead. + */ + +namespace Swan +{ +#pragma warning disable CA1032 // Add standard exception constructors. + /// + /// The exception that is thrown by methods of the class + /// to signal a condition most probably caused by an internal error in a library + /// or application. + /// Do not use this class directly; use the methods of the class instead. + /// + [Serializable] + public sealed class InternalErrorException : Exception + { +#pragma warning disable SA1642 // Constructor summary documentation should begin with standard text - the tag confuses the analyzer. + /// + /// Initializes a new instance of the class. + /// Do not call this constrcutor directly; use the methods of the class instead. + /// + /// The message that describes the error. +#pragma warning disable SA1642 + internal InternalErrorException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + private InternalErrorException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +#pragma warning restore CA1032 +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Logging/ConsoleLogger.cs b/Vendor/Swan.Lite-3.1.0/Logging/ConsoleLogger.cs new file mode 100644 index 0000000..a74bca8 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Logging/ConsoleLogger.cs @@ -0,0 +1,145 @@ +using System; + +namespace Swan.Logging +{ + /// + /// Represents a Console implementation of ILogger. + /// + /// + public class ConsoleLogger : TextLogger, ILogger + { + /// + /// Initializes a new instance of the class. + /// + protected ConsoleLogger() + { + // Empty + } + + /// + /// Gets the current instance of ConsoleLogger. + /// + /// + /// The instance. + /// + public static ConsoleLogger Instance { get; } = new ConsoleLogger(); + + /// + /// Gets or sets the debug logging prefix. + /// + /// + /// The debug prefix. + /// + public static string DebugPrefix { get; set; } = "DBG"; + + /// + /// Gets or sets the trace logging prefix. + /// + /// + /// The trace prefix. + /// + public static string TracePrefix { get; set; } = "TRC"; + + /// + /// Gets or sets the warning logging prefix. + /// + /// + /// The warn prefix. + /// + public static string WarnPrefix { get; set; } = "WRN"; + + /// + /// Gets or sets the fatal logging prefix. + /// + /// + /// The fatal prefix. + /// + public static string FatalPrefix { get; set; } = "FAT"; + + /// + /// Gets or sets the error logging prefix. + /// + /// + /// The error prefix. + /// + public static string ErrorPrefix { get; set; } = "ERR"; + + /// + /// Gets or sets the information logging prefix. + /// + /// + /// The information prefix. + /// + public static string InfoPrefix { get; set; } = "INF"; + + /// + /// Gets or sets the color of the information output logging. + /// + /// + /// The color of the information. + /// + public static ConsoleColor InfoColor { get; set; } = ConsoleColor.Cyan; + + /// + /// Gets or sets the color of the debug output logging. + /// + /// + /// The color of the debug. + /// + public static ConsoleColor DebugColor { get; set; } = ConsoleColor.Gray; + + /// + /// Gets or sets the color of the trace output logging. + /// + /// + /// The color of the trace. + /// + public static ConsoleColor TraceColor { get; set; } = ConsoleColor.DarkGray; + + /// + /// Gets or sets the color of the warning logging. + /// + /// + /// The color of the warn. + /// + public static ConsoleColor WarnColor { get; set; } = ConsoleColor.Yellow; + + /// + /// Gets or sets the color of the error logging. + /// + /// + /// The color of the error. + /// + public static ConsoleColor ErrorColor { get; set; } = ConsoleColor.DarkRed; + + /// + /// Gets or sets the color of the error logging. + /// + /// + /// The color of the error. + /// + public static ConsoleColor FatalColor { get; set; } = ConsoleColor.Red; + + /// + public LogLevel LogLevel { get; set; } = DebugLogger.IsDebuggerAttached ? LogLevel.Trace : LogLevel.Info; + + /// + public void Log(LogMessageReceivedEventArgs logEvent) + { + // Select the writer based on the message type + var writer = logEvent.MessageType == LogLevel.Error + ? TerminalWriters.StandardError + : TerminalWriters.StandardOutput; + + var (outputMessage, color) = GetOutputAndColor(logEvent); + + Terminal.Write(outputMessage, color, writer); + } + + /// + public void Dispose() + { + // Do nothing + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Logging/DebugLogger.cs b/Vendor/Swan.Lite-3.1.0/Logging/DebugLogger.cs new file mode 100644 index 0000000..a71e615 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Logging/DebugLogger.cs @@ -0,0 +1,51 @@ +namespace Swan.Logging +{ + /// + /// Represents a logger target. This target will write to the + /// Debug console using System.Diagnostics.Debug. + /// + /// + public class DebugLogger : TextLogger, ILogger + { + /// + /// Initializes a new instance of the class. + /// + protected DebugLogger() + { + // Empty + } + + /// + /// Gets the current instance of DebugLogger. + /// + /// + /// The instance. + /// + public static DebugLogger Instance { get; } = new DebugLogger(); + + /// + /// Gets a value indicating whether a debugger is attached. + /// + /// + /// true if this instance is debugger attached; otherwise, false. + /// + public static bool IsDebuggerAttached => System.Diagnostics.Debugger.IsAttached; + + /// + public LogLevel LogLevel { get; set; } = IsDebuggerAttached ? LogLevel.Trace : LogLevel.None; + + /// + public void Log(LogMessageReceivedEventArgs logEvent) + { + var (outputMessage, _) = GetOutputAndColor(logEvent); + + System.Diagnostics.Debug.Write(outputMessage); + } + + /// + public void Dispose() + { + // do nothing + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Logging/FileLogger.cs b/Vendor/Swan.Lite-3.1.0/Logging/FileLogger.cs new file mode 100644 index 0000000..eeacb63 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Logging/FileLogger.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Swan.Threading; + +namespace Swan.Logging +{ + /// + /// A helper class to write into files the messages sent by the . + /// + /// + public class FileLogger : TextLogger, ILogger + { + private readonly ManualResetEventSlim _doneEvent = new ManualResetEventSlim(true); + private readonly ConcurrentQueue _logQueue = new ConcurrentQueue(); + private readonly ExclusiveTimer _timer; + private readonly string _filePath; + + private bool _disposedValue; // To detect redundant calls + + /// + /// Initializes a new instance of the class. + /// + public FileLogger() + : this(SwanRuntime.EntryAssemblyDirectory, true) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The filePath. + /// if set to true [daily file]. + public FileLogger(string filePath, bool dailyFile) + { + _filePath = filePath; + DailyFile = dailyFile; + + _timer = new ExclusiveTimer( + async () => await WriteLogEntries().ConfigureAwait(false), + TimeSpan.Zero, + TimeSpan.FromSeconds(5)); + } + + /// + public LogLevel LogLevel { get; set; } + + /// + /// Gets the file path. + /// + /// + /// The file path. + /// + public string FilePath => DailyFile + ? Path.Combine(Path.GetDirectoryName(_filePath), Path.GetFileNameWithoutExtension(_filePath) + $"_{DateTime.UtcNow:yyyyMMdd}" + Path.GetExtension(_filePath)) + : _filePath; + + /// + /// Gets a value indicating whether [daily file]. + /// + /// + /// true if [daily file]; otherwise, false. + /// + public bool DailyFile { get; } + + /// + public void Log(LogMessageReceivedEventArgs logEvent) + { + var (outputMessage, _) = GetOutputAndColor(logEvent); + + _logQueue.Enqueue(outputMessage); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_disposedValue) return; + + if (disposing) + { + _timer.Pause(); + _timer.Dispose(); + + _doneEvent.Wait(); + _doneEvent.Reset(); + WriteLogEntries(true).Await(); + _doneEvent.Dispose(); + } + + _disposedValue = true; + } + + private async Task WriteLogEntries(bool finalCall = false) + { + if (_logQueue.IsEmpty) + return; + + if (!finalCall && !_doneEvent.IsSet) + return; + + _doneEvent.Reset(); + + try + { + using var file = File.AppendText(FilePath); + while (!_logQueue.IsEmpty) + { + if (_logQueue.TryDequeue(out var entry)) + await file.WriteAsync(entry).ConfigureAwait(false); + } + } + finally + { + if (!finalCall) + _doneEvent.Set(); + } + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Logging/ILogger.cs b/Vendor/Swan.Lite-3.1.0/Logging/ILogger.cs new file mode 100644 index 0000000..4dbf6ca --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Logging/ILogger.cs @@ -0,0 +1,24 @@ +namespace Swan.Logging +{ + using System; + + /// + /// Interface for a logger implementation. + /// + public interface ILogger : IDisposable + { + /// + /// Gets the log level. + /// + /// + /// The log level. + /// + LogLevel LogLevel { get; } + + /// + /// Logs the specified log event. + /// + /// The instance containing the event data. + void Log(LogMessageReceivedEventArgs logEvent); + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Logging/LogLevel.cs b/Vendor/Swan.Lite-3.1.0/Logging/LogLevel.cs new file mode 100644 index 0000000..b3d15a8 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Logging/LogLevel.cs @@ -0,0 +1,43 @@ +namespace Swan.Logging +{ + /// + /// Defines the log levels. + /// + public enum LogLevel + { + /// + /// The none message type + /// + None, + + /// + /// The trace message type + /// + Trace, + + /// + /// The debug message type + /// + Debug, + + /// + /// The information message type + /// + Info, + + /// + /// The warning message type + /// + Warning, + + /// + /// The error message type + /// + Error, + + /// + /// The fatal message type + /// + Fatal, + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Logging/LogMessageReceivedEventArgs.cs b/Vendor/Swan.Lite-3.1.0/Logging/LogMessageReceivedEventArgs.cs new file mode 100644 index 0000000..14e01cb --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Logging/LogMessageReceivedEventArgs.cs @@ -0,0 +1,131 @@ +using System; + +namespace Swan.Logging +{ + /// + /// Event arguments representing the message that is logged + /// on to the terminal. Use the properties to forward the data to + /// your logger of choice. + /// + /// + public class LogMessageReceivedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The sequence. + /// Type of the message. + /// The UTC date. + /// The source. + /// The message. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public LogMessageReceivedEventArgs( + ulong sequence, + LogLevel messageType, + DateTime utcDate, + string? source, + string message, + object? extendedData, + string callerMemberName, + string callerFilePath, + int callerLineNumber) + { + Sequence = sequence; + MessageType = messageType; + UtcDate = utcDate; + Source = source; + Message = message; + CallerMemberName = callerMemberName; + CallerFilePath = callerFilePath; + CallerLineNumber = callerLineNumber; + ExtendedData = extendedData; + } + + /// + /// Gets logging message sequence. + /// + /// + /// The sequence. + /// + public ulong Sequence { get; } + + /// + /// Gets the type of the message. + /// It can be a combination as the enumeration is a set of bitwise flags. + /// + /// + /// The type of the message. + /// + public LogLevel MessageType { get; } + + /// + /// Gets the UTC date at which the event at which the message was logged. + /// + /// + /// The UTC date. + /// + public DateTime UtcDate { get; } + + /// + /// Gets the name of the source where the logging message + /// came from. This can come empty if the logger did not set it. + /// + /// + /// The source. + /// + public string? Source { get; } + + /// + /// Gets the body of the message. + /// + /// + /// The message. + /// + public string Message { get; } + + /// + /// Gets the name of the caller member. + /// + /// + /// The name of the caller member. + /// + public string CallerMemberName { get; } + + /// + /// Gets the caller file path. + /// + /// + /// The caller file path. + /// + public string CallerFilePath { get; } + + /// + /// Gets the caller line number. + /// + /// + /// The caller line number. + /// + public int CallerLineNumber { get; } + + /// + /// Gets an object representing extended data. + /// It could be an exception or anything else. + /// + /// + /// The extended data. + /// + public object? ExtendedData { get; } + + /// + /// Gets the Extended Data properties cast as an Exception (if possible) + /// Otherwise, it return null. + /// + /// + /// The exception. + /// + public Exception? Exception => ExtendedData as Exception; + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Logging/Logger.cs b/Vendor/Swan.Lite-3.1.0/Logging/Logger.cs new file mode 100644 index 0000000..1ab0503 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Logging/Logger.cs @@ -0,0 +1,664 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Swan.Logging +{ + /// + /// Entry-point for logging. Use this static class to register/unregister + /// loggers instances. By default, the ConsoleLogger is registered. + /// + public static class Logger + { + private static readonly object SyncLock = new object(); + private static readonly List Loggers = new List(); + + private static ulong _loggingSequence; + + static Logger() + { + if (Terminal.IsConsolePresent) + Loggers.Add(ConsoleLogger.Instance); + + if (DebugLogger.IsDebuggerAttached) + Loggers.Add(DebugLogger.Instance); + } + + #region Standard Public API + + /// + /// Registers the logger. + /// + /// The type of logger to register. + /// There is already a logger with that class registered. + public static void RegisterLogger() + where T : ILogger + { + lock (SyncLock) + { + var loggerInstance = Loggers.FirstOrDefault(x => x.GetType() == typeof(T)); + + if (loggerInstance != null) + throw new InvalidOperationException("There is already a logger with that class registered."); + + Loggers.Add(Activator.CreateInstance()); + } + } + + /// + /// Registers the logger. + /// + /// The logger. + public static void RegisterLogger(ILogger logger) + { + lock (SyncLock) + Loggers.Add(logger); + } + + /// + /// Unregisters the logger. + /// + /// The logger. + /// logger. + public static void UnregisterLogger(ILogger logger) => RemoveLogger(x => x == logger); + + /// + /// Unregisters the logger. + /// + /// The type of logger to unregister. + public static void UnregisterLogger() => RemoveLogger(x => x.GetType() == typeof(T)); + + /// + /// Remove all the loggers. + /// + public static void NoLogging() + { + lock (SyncLock) + Loggers.Clear(); + } + + #region Debug + + /// + /// Logs a debug message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Debug( + this string message, + string? source = null, + object? extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Debug, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a debug message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Debug( + this string message, + Type source, + object? extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Debug, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a debug message to the console. + /// + /// The exception. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Debug( + this Exception extendedData, + string source, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Debug, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + + #region Trace + + /// + /// Logs a trace message to the console. + /// + /// The text. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Trace( + this string message, + string? source = null, + object? extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Trace, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a trace message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Trace( + this string message, + Type source, + object? extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Trace, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a trace message to the console. + /// + /// The extended data. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Trace( + this Exception extendedData, + string source, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Trace, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + + #region Warn + + /// + /// Logs a warning message to the console. + /// + /// The text. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Warn( + this string message, + string? source = null, + object? extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Warning, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a warning message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Warn( + this string message, + Type source, + object? extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Warning, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a warning message to the console. + /// + /// The extended data. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Warn( + this Exception extendedData, + string source, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Warning, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + + #region Fatal + + /// + /// Logs a warning message to the console. + /// + /// The text. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Fatal( + this string message, + string? source = null, + object? extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Fatal, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a warning message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Fatal( + this string message, + Type source, + object? extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Fatal, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a warning message to the console. + /// + /// The extended data. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Fatal( + this Exception extendedData, + string source, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Fatal, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + + #region Info + + /// + /// Logs an info message to the console. + /// + /// The text. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Info( + this string message, + string? source = null, + object? extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Info, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs an info message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Info( + this string message, + Type source, + object? extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Info, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs an info message to the console. + /// + /// The extended data. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Info( + this Exception extendedData, + string source, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Info, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + + #region Error + + /// + /// Logs an error message to the console's standard error. + /// + /// The text. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Error( + this string message, + string? source = null, + object? extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Error, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs an error message to the console's standard error. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Error( + this string message, + Type source, + object? extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Error, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs an error message to the console's standard error. + /// + /// The exception. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Error( + this Exception ex, + string source, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Error, message, source, ex, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + + #endregion + + #region Extended Public API + + /// + /// Logs the specified message. + /// + /// The message. + /// The source. + /// Type of the message. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Log( + this string message, + string source, + LogLevel messageType, + object? extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(messageType, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs the specified message. + /// + /// The message. + /// The source. + /// Type of the message. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Log( + this string message, + Type source, + LogLevel messageType, + object? extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(messageType, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs an error message to the console's standard error. + /// + /// The ex. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Log( + this Exception ex, + string? source = null, + string? message = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + if (ex is null) + return; + + LogMessage(LogLevel.Error, message ?? ex.Message, source ?? ex.Source, ex, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs an error message to the console's standard error. + /// + /// The ex. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Log( + this Exception ex, + Type? source = null, + string? message = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + if (ex is null) + return; + + LogMessage(LogLevel.Error, message ?? ex.Message, source?.FullName ?? ex.Source, ex, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a trace message showing all possible non-null properties of the given object + /// This method is expensive as it uses Stringify internally. + /// + /// The object. + /// The source. + /// The title. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Dump( + this object? obj, + string source, + string text = "Object Dump", + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + if (obj == null) + return; + + var message = $"{text} ({obj.GetType()}): {Environment.NewLine}{obj.Stringify().Indent(5)}"; + LogMessage(LogLevel.Trace, message, source, obj, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a trace message showing all possible non-null properties of the given object + /// This method is expensive as it uses Stringify internally. + /// + /// The object. + /// The source. + /// The text. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Dump( + this object? obj, + Type source, + string text = "Object Dump", + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + if (obj == null) + return; + + var message = $"{text} ({obj.GetType()}): {Environment.NewLine}{obj.Stringify().Indent(5)}"; + LogMessage(LogLevel.Trace, message, source?.FullName, obj, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + + private static void RemoveLogger(Func criteria) + { + lock (SyncLock) + { + var loggerInstance = Loggers.FirstOrDefault(criteria); + + if (loggerInstance == null) + throw new InvalidOperationException("The logger is not registered."); + + loggerInstance.Dispose(); + + Loggers.Remove(loggerInstance); + } + } + + private static void LogMessage( + LogLevel logLevel, + string message, + string? sourceName, + object? extendedData, + string callerMemberName, + string callerFilePath, + int callerLineNumber) + { + var sequence = _loggingSequence; + var date = DateTime.UtcNow; + _loggingSequence++; + + var loggerMessage = string.IsNullOrWhiteSpace(message) ? + string.Empty : message.RemoveControlChars('\n'); + + var eventArgs = new LogMessageReceivedEventArgs( + sequence, + logLevel, + date, + sourceName, + loggerMessage, + extendedData, + callerMemberName, + callerFilePath, + callerLineNumber); + + foreach (var logger in Loggers) + { + Task.Run(() => + { + if (logger.LogLevel <= logLevel) + logger.Log(eventArgs); + }); + } + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Logging/TextLogger.cs b/Vendor/Swan.Lite-3.1.0/Logging/TextLogger.cs new file mode 100644 index 0000000..9d63efb --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Logging/TextLogger.cs @@ -0,0 +1,79 @@ +using System; + +namespace Swan.Logging +{ + /// + /// Use this class for text-based logger. + /// + public abstract class TextLogger + { + /// + /// Gets or sets the logging time format. + /// set to null or empty to prevent output. + /// + /// + /// The logging time format. + /// + public static string LoggingTimeFormat { get; set; } = "HH:mm:ss.fff"; + + /// + /// Gets the color of the output of the message (the output message has a new line char in the end). + /// + /// The instance containing the event data. + /// + /// The output message formatted and the color of the console to be used. + /// + protected (string outputMessage, ConsoleColor color) GetOutputAndColor(LogMessageReceivedEventArgs logEvent) + { + var (prefix , color) = GetConsoleColorAndPrefix(logEvent.MessageType); + + var loggerMessage = string.IsNullOrWhiteSpace(logEvent.Message) + ? string.Empty + : logEvent.Message.RemoveControlChars('\n'); + + var outputMessage = CreateOutputMessage(logEvent.Source, loggerMessage, prefix, logEvent.UtcDate); + + // Further format the output in the case there is an exception being logged + if (logEvent.MessageType == LogLevel.Error && logEvent.Exception != null) + { + try + { + outputMessage += $"{logEvent.Exception.Stringify().Indent()}{Environment.NewLine}"; + } + catch + { + // Ignore + } + } + + return (outputMessage, color); + } + + private static (string Prefix, ConsoleColor color) GetConsoleColorAndPrefix(LogLevel messageType) => + messageType switch + { + LogLevel.Debug => (ConsoleLogger.DebugPrefix, ConsoleLogger.DebugColor), + LogLevel.Error => (ConsoleLogger.ErrorPrefix, ConsoleLogger.ErrorColor), + LogLevel.Info => (ConsoleLogger.InfoPrefix, ConsoleLogger.InfoColor), + LogLevel.Trace => (ConsoleLogger.TracePrefix, ConsoleLogger.TraceColor), + LogLevel.Warning => (ConsoleLogger.WarnPrefix, ConsoleLogger.WarnColor), + LogLevel.Fatal => (ConsoleLogger.FatalPrefix, ConsoleLogger.FatalColor), + _ => (new string(' ', ConsoleLogger.InfoPrefix.Length), Terminal.Settings.DefaultColor) + }; + + private static string CreateOutputMessage(string sourceName, string loggerMessage, string prefix, DateTime date) + { + var friendlySourceName = string.IsNullOrWhiteSpace(sourceName) + ? string.Empty + : sourceName.SliceLength(sourceName.LastIndexOf('.') + 1, sourceName.Length); + + var outputMessage = string.IsNullOrWhiteSpace(sourceName) + ? loggerMessage + : $"[{friendlySourceName}] {loggerMessage}"; + + return string.IsNullOrWhiteSpace(LoggingTimeFormat) + ? $" {prefix} >> {outputMessage}{Environment.NewLine}" + : $" {date.ToLocalTime().ToString(LoggingTimeFormat)} {prefix} >> {outputMessage}{Environment.NewLine}"; + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Mappers/CopyableAttribute.cs b/Vendor/Swan.Lite-3.1.0/Mappers/CopyableAttribute.cs new file mode 100644 index 0000000..4f04954 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Mappers/CopyableAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Swan.Mappers +{ + /// + /// Represents an attribute to select which properties are copyable between objects. + /// + /// + [AttributeUsage(AttributeTargets.Property)] + public class CopyableAttribute : Attribute + { + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Mappers/IObjectMap.cs b/Vendor/Swan.Lite-3.1.0/Mappers/IObjectMap.cs new file mode 100644 index 0000000..77a3c1b --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Mappers/IObjectMap.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Swan.Mappers +{ + /// + /// Interface object map. + /// + public interface IObjectMap + { + /// + /// Gets or sets the map. + /// + Dictionary> Map { get; } + + /// + /// Gets or sets the type of the source. + /// + Type SourceType { get; } + + /// + /// Gets or sets the type of the destination. + /// + Type DestinationType { get; } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Mappers/ObjectMap.cs b/Vendor/Swan.Lite-3.1.0/Mappers/ObjectMap.cs new file mode 100644 index 0000000..1774023 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Mappers/ObjectMap.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace Swan.Mappers +{ + /// + /// Represents an object map. + /// + /// The type of the source. + /// The type of the destination. + /// + public class ObjectMap : IObjectMap + { + internal ObjectMap(IEnumerable intersect) + { + SourceType = typeof(TSource); + DestinationType = typeof(TDestination); + Map = intersect.ToDictionary( + property => DestinationType.GetProperty(property.Name), + property => new List {SourceType.GetProperty(property.Name)}); + } + + /// + public Dictionary> Map { get; } + + /// + public Type SourceType { get; } + + /// + public Type DestinationType { get; } + + /// + /// Maps the property. + /// + /// The type of the destination property. + /// The type of the source property. + /// The destination property. + /// The source property. + /// + /// An object map representation of type of the destination property + /// and type of the source property. + /// + public ObjectMap MapProperty + ( + Expression> destinationProperty, + Expression> sourceProperty) + { + if (destinationProperty == null) + throw new ArgumentNullException(nameof(destinationProperty)); + + var propertyDestinationInfo = (destinationProperty.Body as MemberExpression)?.Member as PropertyInfo; + + if (propertyDestinationInfo == null) + throw new ArgumentException("Invalid destination expression", nameof(destinationProperty)); + + var sourceMembers = GetSourceMembers(sourceProperty); + + if (!sourceMembers.Any()) + throw new ArgumentException("Invalid source expression", nameof(sourceProperty)); + + // reverse order + sourceMembers.Reverse(); + Map[propertyDestinationInfo] = sourceMembers; + + return this; + } + + /// + /// Removes the map property. + /// + /// The type of the destination property. + /// The destination property. + /// + /// An object map representation of type of the destination property + /// and type of the source property. + /// + /// Invalid destination expression. + public ObjectMap RemoveMapProperty( + Expression> destinationProperty) + { + if (destinationProperty == null) + throw new ArgumentNullException(nameof(destinationProperty)); + + var propertyDestinationInfo = (destinationProperty.Body as MemberExpression)?.Member as PropertyInfo; + + if (propertyDestinationInfo == null) + throw new ArgumentException("Invalid destination expression", nameof(destinationProperty)); + + if (Map.ContainsKey(propertyDestinationInfo)) + { + Map.Remove(propertyDestinationInfo); + } + + return this; + } + + private static List GetSourceMembers(Expression> sourceProperty) + { + if (sourceProperty == null) + throw new ArgumentNullException(nameof(sourceProperty)); + + var sourceMembers = new List(); + var initialExpression = sourceProperty.Body as MemberExpression; + + while (true) + { + var propertySourceInfo = initialExpression?.Member as PropertyInfo; + + if (propertySourceInfo == null) break; + sourceMembers.Add(propertySourceInfo); + initialExpression = initialExpression.Expression as MemberExpression; + } + + return sourceMembers; + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Mappers/ObjectMapper.PropertyInfoComparer.cs b/Vendor/Swan.Lite-3.1.0/Mappers/ObjectMapper.PropertyInfoComparer.cs new file mode 100644 index 0000000..bd5f1a7 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Mappers/ObjectMapper.PropertyInfoComparer.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Reflection; + +namespace Swan.Mappers +{ + /// + /// Represents an AutoMapper-like object to map from one object type + /// to another using defined properties map or using the default behaviour + /// to copy same named properties from one object to another. + /// + /// The extension methods like CopyPropertiesTo use the default behaviour. + /// + public partial class ObjectMapper + { + internal class PropertyInfoComparer : IEqualityComparer + { + public bool Equals(PropertyInfo x, PropertyInfo y) + => x != null && y != null && x.Name == y.Name && x.PropertyType == y.PropertyType; + + public int GetHashCode(PropertyInfo obj) + => obj.Name.GetHashCode() + obj.PropertyType.Name.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Mappers/ObjectMapper.cs b/Vendor/Swan.Lite-3.1.0/Mappers/ObjectMapper.cs new file mode 100644 index 0000000..11e000d --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Mappers/ObjectMapper.cs @@ -0,0 +1,374 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Swan.Reflection; + +namespace Swan.Mappers +{ + /// + /// Represents an AutoMapper-like object to map from one object type + /// to another using defined properties map or using the default behaviour + /// to copy same named properties from one object to another. + /// + /// The extension methods like CopyPropertiesTo use the default behaviour. + /// + /// + /// The following code explains how to map an object's properties into an instance of type T. + /// + /// using Swan.Mappers; + /// + /// class Example + /// { + /// class Person + /// { + /// public string Name { get; set; } + /// public int Age { get; set; } + /// } + /// + /// static void Main() + /// { + /// var obj = new { Name = "John", Age = 42 }; + /// + /// var person = Runtime.ObjectMapper.Map<Person>(obj); + /// } + /// } + /// + /// + /// The following code explains how to explicitly map certain properties. + /// + /// using Swan.Mappers; + /// + /// class Example + /// { + /// class User + /// { + /// public string Name { get; set; } + /// public Role Role { get; set; } + /// } + /// + /// public class Role + /// { + /// public string Name { get; set; } + /// } + /// + /// class UserDto + /// { + /// public string Name { get; set; } + /// public string Role { get; set; } + /// } + /// + /// static void Main() + /// { + /// // create a User object + /// var person = + /// new User { Name = "Phillip", Role = new Role { Name = "Admin" } }; + /// + /// // create an Object Mapper + /// var mapper = new ObjectMapper(); + /// + /// // map the User's Role.Name to UserDto's Role + /// mapper.CreateMap<User, UserDto>() + /// .MapProperty(d => d.Role, x => x.Role.Name); + /// + /// // apply the previous map and retrieve a UserDto object + /// var destination = mapper.Map<UserDto>(person); + /// } + /// } + /// + /// + public partial class ObjectMapper + { + private static readonly Lazy LazyInstance = new Lazy(() => new ObjectMapper()); + + private readonly List _maps = new List(); + + /// + /// Gets the current. + /// + /// + /// The current. + /// + public static ObjectMapper Current => LazyInstance.Value; + + /// + /// Copies the specified source. + /// + /// The source. + /// The target. + /// The properties to copy. + /// The ignore properties. + /// + /// Copied properties count. + /// + /// + /// source + /// or + /// target. + /// + public static int Copy( + object source, + object target, + IEnumerable? propertiesToCopy = null, + params string[]? ignoreProperties) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + if (target == null) + throw new ArgumentNullException(nameof(target)); + + return CopyInternal( + target, + GetSourceMap(source), + propertiesToCopy, + ignoreProperties); + } + + /// + /// Copies the specified source. + /// + /// The source. + /// The target. + /// The properties to copy. + /// The ignore properties. + /// + /// Copied properties count. + /// + /// + /// source + /// or + /// target. + /// + public static int Copy( + IDictionary? source, + object? target, + IEnumerable? propertiesToCopy = null, + params string[] ignoreProperties) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + if (target == null) + throw new ArgumentNullException(nameof(target)); + + return CopyInternal( + target, + source.ToDictionary( + x => x.Key.ToLowerInvariant(), + x => Tuple.Create(typeof(object), x.Value)), + propertiesToCopy, + ignoreProperties); + } + + /// + /// Creates the map. + /// + /// The type of the source. + /// The type of the destination. + /// + /// An object map representation of type of the destination property + /// and type of the source property. + /// + /// + /// You can't create an existing map + /// or + /// Types doesn't match. + /// + public ObjectMap CreateMap() + { + if (_maps.Any(x => x.SourceType == typeof(TSource) && x.DestinationType == typeof(TDestination))) + throw new InvalidOperationException("You can't create an existing map"); + + var sourceType = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(true); + var destinationType = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(true); + + var intersect = sourceType.Intersect(destinationType, new PropertyInfoComparer()).ToArray(); + + if (!intersect.Any()) + throw new InvalidOperationException("Types doesn't match"); + + var map = new ObjectMap(intersect); + + _maps.Add(map); + + return map; + } + + /// + /// Maps the specified source. + /// + /// The type of the destination. + /// The source. + /// if set to true [automatic resolve]. + /// + /// A new instance of the map. + /// + /// source. + /// You can't map from type {source.GetType().Name} to {typeof(TDestination).Name}. + public TDestination Map(object source, bool autoResolve = true) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + var destination = Activator.CreateInstance(); + var map = _maps + .FirstOrDefault(x => x.SourceType == source.GetType() && x.DestinationType == typeof(TDestination)); + + if (map != null) + { + foreach (var property in map.Map) + { + var finalSource = property.Value.Aggregate(source, + (current, sourceProperty) => sourceProperty.GetValue(current)); + + property.Key.SetValue(destination, finalSource); + } + } + else + { + if (!autoResolve) + { + throw new InvalidOperationException( + $"You can't map from type {source.GetType().Name} to {typeof(TDestination).Name}"); + } + + // Missing mapping, try to use default behavior + Copy(source, destination!); + } + + return destination; + } + + private static int CopyInternal( + object target, + Dictionary> sourceProperties, + IEnumerable? propertiesToCopy, + IEnumerable? ignoreProperties) + { + // Filter properties + var requiredProperties = propertiesToCopy? + .Where(p => !string.IsNullOrWhiteSpace(p)) + .Select(p => p.ToLowerInvariant()); + + var ignoredProperties = ignoreProperties? + .Where(p => !string.IsNullOrWhiteSpace(p)) + .Select(p => p.ToLowerInvariant()); + + var properties = PropertyTypeCache.DefaultCache.Value + .RetrieveFilteredProperties(target.GetType(), true, x => x.CanWrite); + + return properties + .Select(x => x.Name) + .Distinct() + .ToDictionary(x => x.ToLowerInvariant(), x => properties.First(y => y.Name == x)) + .Where(x => sourceProperties.Keys.Contains(x.Key)) + .When(() => requiredProperties != null, q => q.Where(y => requiredProperties!.Contains(y.Key))) + .When(() => ignoredProperties != null, q => q.Where(y => !ignoredProperties!.Contains(y.Key))) + .ToDictionary(x => x.Value, x => sourceProperties[x.Key]) + .Sum(x => TrySetValue(x.Key, x.Value, target) ? 1 : 0); + } + + private static bool TrySetValue(PropertyInfo propertyInfo, Tuple property, object target) + { + try + { + var (type, value) = property; + + if (type.IsEnum) + { + propertyInfo.SetValue(target, + Enum.ToObject(propertyInfo.PropertyType, value)); + + return true; + } + + if (type.IsValueType || propertyInfo.PropertyType != type) + return propertyInfo.TrySetBasicType(value, target); + + if (propertyInfo.PropertyType.IsArray) + { + propertyInfo.TrySetArray(value as IEnumerable, target); + return true; + } + + propertyInfo.SetValue(target, GetValue(value, propertyInfo.PropertyType)); + + return true; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // swallow + } + + return false; + } + + private static object? GetValue(object source, Type targetType) + { + if (source == null) + return null; + + object? target = null; + + source.CreateTarget(targetType, false, ref target); + + switch (source) + { + case string _: + target = source; + break; + case IList sourceList when target is IList targetList: + var addMethod = targetType.GetMethods() + .FirstOrDefault( + m => m.Name == Formatters.Json.AddMethodName && m.IsPublic && m.GetParameters().Length == 1); + + if (addMethod == null) return target; + + var isItemValueType = targetList.GetType().GetElementType().IsValueType; + + foreach (var item in sourceList) + { + try + { + targetList.Add(isItemValueType + ? item + : item.CopyPropertiesToNew()); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // ignored + } + } + + break; + default: + source.CopyPropertiesTo(target!); + break; + } + + return target; + } + + private static Dictionary> GetSourceMap(object source) + { + // select distinct properties because they can be duplicated by inheritance + var sourceProperties = PropertyTypeCache.DefaultCache.Value + .RetrieveFilteredProperties(source.GetType(), true, x => x.CanRead) + .ToArray(); + + return sourceProperties + .Select(x => x.Name) + .Distinct() + .ToDictionary( + x => x.ToLowerInvariant(), + x => Tuple.Create(sourceProperties.First(y => y.Name == x).PropertyType, + sourceProperties.First(y => y.Name == x).GetValue(source))); + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Net/IPAddressRange.cs b/Vendor/Swan.Lite-3.1.0/Net/IPAddressRange.cs new file mode 100644 index 0000000..d09cb6f --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Net/IPAddressRange.cs @@ -0,0 +1,552 @@ +using System; +using System.Globalization; +using System.Net; +using System.Net.Sockets; +using Swan.Net.Internal; + +namespace Swan.Net +{ + // NOTE TO CONTRIBUTORS: When adding a check on a public method parameter, + // please do not just "throw new ArgumentException(...)". + // Instead, look at the exception-returning private methods at the bottom of this file + // and either find one suitable for your case, or add a new one. + // This way we can keep the exception messages consistent. + + /// + /// Represents an inclusive range of IP addresses. + /// + /// + /// This class makes no distinction between IPv4 addresses and the same addresses mapped to IPv6 + /// for the purpose of determining whether it belongs to a range: that is, the method + /// of an instance initialized with IPv4 addresses, or with the same addresses mapped to IPv6, + /// will return for both an in-range IPv4 address and the same address mapped to IPv6. + /// The constructor, however, + /// does make such distinction: you cannot initialize a range using an IPv4 address and an IPv6 address, + /// even if the latter is an IPv4 address mapped to IPv6, nor the other way around. + /// + /// + [Serializable] + public sealed class IPAddressRange : IEquatable + { + /// + /// Gets an instance of that contains no addresses. + /// The method of the returned instance will always return . + /// This property is useful to initialize non-nullable properties + /// of type . + /// + public static readonly IPAddressRange None = new IPAddressRange(IPAddressValue.MaxValue, IPAddressValue.MinValue, true, 0); + + /// + /// Gets an instance of that contains all possible IP addresses. + /// The method of the returned instance will always return . + /// + public static readonly IPAddressRange All = new IPAddressRange(IPAddressValue.MinValue, IPAddressValue.MaxValue, true, 128); + + /// + /// Gets an instance of that contains all IPv4 addresses. + /// The method of the returned instance will return + /// for all IPv4 addresses, as well as their IPv6 mapped counterparts, and + /// for all other IPv6 addresses. + /// + public static readonly IPAddressRange AllIPv4 = new IPAddressRange(IPAddressValue.MinIPv4Value, IPAddressValue.MaxIPv4Value, false, 32); + + private readonly IPAddressValue _start; + private readonly IPAddressValue _end; + private readonly bool _isV6; + private readonly byte _prefixLength; + + /// + /// Initializes a new instance of the class, + /// representing a single IP address. + /// + /// The IP address. + /// is . + public IPAddressRange(IPAddress address) + { + if (address == null) + throw new ArgumentNullException(nameof(address)); + + _start = _end = new IPAddressValue(address); + _isV6 = address.AddressFamily == AddressFamily.InterNetworkV6; + _prefixLength = 0; + } + + /// + /// Initializes a new instance of the class, + /// representing a range of IP addresses between + /// and , extremes included. + /// + /// The starting address of the range. + /// The ending address of the range. + /// + /// is . + /// - or - + /// is . + /// + /// + /// has a different AddressFamily + /// from . + /// - or - + /// is a lower address than , + /// i.e. the binary representation of in network byte order + /// is a lower number than the same representation of . + /// + public IPAddressRange(IPAddress start, IPAddress end) + { + if (start == null) + throw new ArgumentNullException(nameof(start)); + + if (end == null) + throw new ArgumentNullException(nameof(end)); + + var startFamily = start.AddressFamily; + _isV6 = startFamily == AddressFamily.InterNetworkV6; + if (end.AddressFamily != startFamily) + throw MismatchedEndFamily(nameof(end)); + + _start = new IPAddressValue(start); + _end = new IPAddressValue(end); + if (_end.CompareTo(_start) < 0) + throw EndLowerThanStart(nameof(end)); + + _prefixLength = 0; + } + + /// + /// Initializes a new instance of the class, + /// representing a CIDR subnet. + /// + /// The base address of the subnet. + /// The prefix length of the subnet. + /// is . + /// + /// is zero. + /// - or - + /// is greater than the number of bits in + /// the binary representation of (32 for IPv4 addresses, + /// 128 for IPv6 addresses.) + /// - or - + /// cannot be the base address of a subnet with a prefix length + /// equal to , because the remaining bits after the prefix + /// are not all zeros. + /// + public IPAddressRange(IPAddress baseAddress, byte prefixLength) + { + if (baseAddress == null) + throw new ArgumentNullException(nameof(baseAddress)); + + byte maxPrefixLength; + if (baseAddress.AddressFamily == AddressFamily.InterNetworkV6) + { + _isV6 = true; + maxPrefixLength = 128; + } + else + { + _isV6 = false; + maxPrefixLength = 32; + } + + if (prefixLength < 1 || prefixLength > maxPrefixLength) + throw InvalidPrefixLength(nameof(prefixLength)); + + _start = new IPAddressValue(baseAddress); + if (!_start.IsStartOfSubnet(prefixLength)) + throw InvalidSubnetBaseAddress(nameof(baseAddress)); + + _end = _start.GetEndOfSubnet(prefixLength); + _prefixLength = prefixLength; + } + + private IPAddressRange(IPAddressValue start, IPAddressValue end, bool isV6, byte prefixLength) + { + _start = start; + _end = end; + _isV6 = isV6; + _prefixLength = prefixLength; + } + + /// + /// Gets the address family of the IP address range. + /// + /// + /// Regardless of the value of this property, IPv4 addresses + /// and their IPv6 mapped counterparts will be considered the same + /// for the purposes of the method. + /// + public AddressFamily AddressFamily => _isV6 ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork; + + /// + /// Gets a value indicating whether this instance represents a CIDR subnet. + /// + /// + /// This property is only for instances + /// initialized via the constructor. + /// Instances constructed by specifying a range will have this property + /// set to even when they actually represent a subnet. + /// For example, the instance returned by IPAddressRange.Parse("192.168.0.0-192.168.0.255") + /// will have this property set to ; for this property to be , + /// the string passed to should instead be "192.168.0.0/24" + /// (a CIDR subnet specification) or "192.168.0.0/255.255.255.0" (a base address / netmask pair, + /// only accepted by and for IPv4 addresses.) + /// + public bool IsSubnet => _prefixLength > 0; + + /// + /// Gets an instance of representing + /// the first address in the range. + /// + public IPAddress Start => _start.ToIPAddress(_isV6); + + /// + /// Gets an instance of representing + /// the last address in the range. + /// + public IPAddress End => _end.ToIPAddress(_isV6); + + /// + /// Tries to convert the string representation of a range of IP addresses + /// to an instance of . + /// + /// The string to convert. + /// When this method returns , + /// an instance of representing the same range of + /// IP addresses represented by . + /// if the conversion was successful; + /// otherwise, . + /// See the "Remarks" section of + /// for an overview of the formats accepted for . + /// + public static bool TryParse(string str, out IPAddressRange result) + => TryParseInternal(nameof(str), str, out result) == null; + + /// + /// Converts the string representation of a range of IP addresses + /// to an instance of . + /// + /// The string to convert. + /// An instance of representing the same range of + /// IP addresses represented by . + /// is . + /// is in none of the supported formats. + /// + /// This method supports the following formats for : + /// + /// + /// Format + /// Description + /// Examples + /// + /// + /// Single address + /// A single IP address. + /// + /// 192.168.23.199 + /// 2001:db8:a0b:12f0::1 + /// + /// + /// + /// Range of addresses + /// Start and end address, separated by a hyphen (-). + /// + /// 192.168.0.100-192.168.11.255 + /// 2001:db8:a0b:12f0::-2001:db8:a0b:12f0::ffff + /// + /// + /// + /// CIDR subnet + /// Base address and prefix length, separated by a slash (/). + /// + /// 169.254.0.0/16 + /// 192.168.123.0/24 + /// 2001:db8:a0b:12f0::/64 + /// + /// + /// + /// "Legacy" subnet + /// + /// Base address and netmask, separated by a slash (/). + /// Only accepted for IPv4 addresses. + /// + /// + /// 169.254.0.0/255.255.0.0 + /// 192.168.123.0/255.255.255.0 + /// + /// + /// + /// + /// + public static IPAddressRange Parse(string str) + { + var exception = TryParseInternal(nameof(str), str, out var result); + if (exception != null) + throw exception; + + return result; + } + + /// + /// + /// The result of this method will be a string that, + /// if passed to the or method, + /// will result in an instance identical to this one. + /// If this instance has been created by means of the + /// or method, the returned string will not + /// necessarily be identical to the parsed string. The possible differences + /// include the following: + /// + /// ranges consisting of just one IP address will result in a + /// string representing that single address; + /// addresses in the returned string are passed to the + /// method, resulting in standardized + /// representations that may be different from the originally parsed + /// strings; + /// the returned string will contain no blank characters; + /// address ranges parsed as address/netmask will be + /// rendered as CIDR subnets: for example, + /// IPAddressRange.Parse("192.168.19.0/255.255.255.0").ToString() + /// will return "192.168.19.0/24". + /// + /// + public override string ToString() + => _prefixLength > 0 + ? $"{Start}/{_prefixLength}" + : _start.CompareTo(_end) == 0 + ? Start.ToString() + : $"{Start}-{End}"; + + /// + /// Determines whether the given + /// sa contained in this range. + /// + /// The IP address to check. + /// if + /// is between and , inclusive; + /// otherwise, . + /// is . + /// + /// This method treats IPv4 addresses and their IPv6-mapped counterparts + /// the same; that is, given a range obtained by parsing the string 192.168.1.0/24, + /// Contains(IPAddress.Parse("192.168.1.55")) will return , + /// as will Contains(IPAddress.Parse("192.168.1.55").MapToIPv6()). This is true + /// as well if a range is initialized with IPv6 addresses. + /// + public bool Contains(IPAddress address) + { + if (address == null) + throw new ArgumentNullException(nameof(address)); + + var addressValue = new IPAddressValue(address); + return addressValue.CompareTo(_start) >= 0 + && addressValue.CompareTo(_end) <= 0; + } + + /// + public override bool Equals(object? obj) => obj is IPAddressRange other && Equals(other); + + /// + public bool Equals(IPAddressRange? other) + => other != null + && other._start.Equals(_start) + && other._end.Equals(_end) + && other._isV6 == _isV6 + && other._prefixLength == _prefixLength; + + /// + public override int GetHashCode() => CompositeHashCode.Using(_start, _end, _isV6, _prefixLength); + + private static bool TryNetmaskToCidrPrefixLength(byte[] bytes, out byte result) + { + result = 0; + var length = bytes.Length; + var prefixFound = false; + for (var i = 0; i < length; i++) + { + if (prefixFound) + { + if (bytes[i] != 0) + return false; + } + else + { + switch (bytes[i]) + { + case 0x00: + if (result == 0) + return false; + + prefixFound = true; + break; + case 0x80: + result += 1; + prefixFound = true; + break; + case 0xC0: + result += 2; + prefixFound = true; + break; + case 0xE0: + result += 3; + prefixFound = true; + break; + case 0xF0: + result += 4; + prefixFound = true; + break; + case 0xF8: + result += 5; + prefixFound = true; + break; + case 0xFC: + result += 6; + prefixFound = true; + break; + case 0xFE: + result += 7; + prefixFound = true; + break; + case 0xFF: + result += 8; + break; + default: + return false; + } + } + } + + return true; + } + + private static Exception? TryParseInternal(string paramName, string? str, out IPAddressRange result) + { + result = None; + + if (str == null) + return new ArgumentNullException(paramName); + + // Try CIDR format (e.g. 192.168.99.0/24) and address/netmask format (192.168.99.0/255.255.255.0) + var separatorPos = str.IndexOf('/'); + if (separatorPos >= 0) + return TryParseCidrOrAddressNetmaskFormat(str, separatorPos, out result); + + // Try range format (e.g. 192.168.99.100-192.168.99.199) + separatorPos = str.IndexOf('-'); + if (separatorPos >= 0) + return TryParseStartEndFormat(str, separatorPos, out result); + + // Try single address format (e.g. 192.168.99.123) + return TryParseSingleAddressFormat(str, out result); + } + + private static Exception? TryParseCidrOrAddressNetmaskFormat(string str, int separatorPos, out IPAddressRange result) + { + result = None; + + var s = str.Substring(0, separatorPos).Trim(); + if (!IPAddressUtility.TryParse(s, out var address)) + return InvalidIPAddress(); + + var addressValue = new IPAddressValue(address); + + s = str.Substring(separatorPos + 1).Trim(); + if (byte.TryParse(s, NumberStyles.None, CultureInfo.InvariantCulture, out var prefixLength)) + { + var maxPrefixLength = address.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32; + if (prefixLength < 1 || prefixLength > maxPrefixLength) + return InvalidPrefixLength(); + + if (!addressValue.IsStartOfSubnet(prefixLength)) + return InvalidSubnetBaseAddress(); + + result = new IPAddressRange(addressValue, addressValue.GetEndOfSubnet(prefixLength), address.AddressFamily == AddressFamily.InterNetworkV6, prefixLength); + return null; + } + + // Only accept a netmask for IPv4 + if (address.AddressFamily != AddressFamily.InterNetwork) + return InvalidPrefixLength(); + + if (!IPAddressUtility.TryParse(s, out var netmask)) + return InvalidPrefixLengthOrNetmask(); + + var addressFamily = address.AddressFamily; + if (netmask.AddressFamily != addressFamily) + return MismatchedNetmaskAddressFamily(); + + var netmaskBytes = netmask.GetAddressBytes(); + if (!TryNetmaskToCidrPrefixLength(netmaskBytes, out prefixLength)) + return InvalidNetmask(); + + if (!addressValue.IsStartOfSubnet(prefixLength)) + return InvalidSubnetBaseAddress(); + + result = new IPAddressRange(addressValue, addressValue.GetEndOfSubnet(prefixLength), false, prefixLength); + return null; + } + + private static Exception? TryParseStartEndFormat(string str, int separatorPos, out IPAddressRange result) + { + result = None; + + var s = str.Substring(0, separatorPos).Trim(); + if (!IPAddressUtility.TryParse(s, out var startAddress)) + return InvalidStartAddress(); + + s = str.Substring(separatorPos + 1).Trim(); + if (!IPAddressUtility.TryParse(s, out var endAddress)) + return InvalidEndAddress(); + + var addressFamily = startAddress.AddressFamily; + if (endAddress.AddressFamily != addressFamily) + return MismatchedStartEndFamily(); + + var start = new IPAddressValue(startAddress); + var end = new IPAddressValue(endAddress); + if (end.CompareTo(start) < 0) + return EndLowerThanStart(); + + result = new IPAddressRange(start, end, addressFamily == AddressFamily.InterNetworkV6, 0); + return null; + } + + private static Exception? TryParseSingleAddressFormat(string str, out IPAddressRange result) + { + result = None; + + if (!IPAddressUtility.TryParse(str, out var address)) + return InvalidIPAddress(); + + var addressValue = new IPAddressValue(address); + result = new IPAddressRange(addressValue, addressValue, address.AddressFamily == AddressFamily.InterNetworkV6, 0); + return null; + } + + private static Exception InvalidIPAddress() => new FormatException("An invalid IP address was specified."); + + private static Exception InvalidPrefixLengthOrNetmask() => new FormatException("An invalid prefix length or netmask was specified."); + + private static Exception MismatchedNetmaskAddressFamily() => new FormatException("Address and netmask are different types of addresses."); + + private static Exception InvalidPrefixLength() => new FormatException("An invalid prefix length was specified."); + + private static Exception InvalidPrefixLength(string paramName) => new ArgumentException("The prefix length is invalid.", paramName); + + private static Exception InvalidNetmask() => new FormatException("An invalid netmask was specified."); + + private static Exception InvalidSubnetBaseAddress() => new FormatException("The specified address is not the base address of the specified subnet."); + + private static Exception InvalidSubnetBaseAddress(string paramName) => new ArgumentException("The specified address is not the base address of the specified subnet.", paramName); + + private static Exception InvalidStartAddress() => new FormatException("An invalid start address was specified for a range."); + + private static Exception InvalidEndAddress() => new FormatException("An invalid end address was specified for a range."); + + private static Exception MismatchedStartEndFamily() => new FormatException("Start and end are different types of addresses."); + + private static Exception MismatchedEndFamily(string paramName) => new ArgumentException("The end address of a range must be of the same family as the start address.", paramName); + + private static Exception EndLowerThanStart() => new FormatException("An end address was specified for a range that is lower than the start address."); + + private static Exception EndLowerThanStart(string paramName) => new ArgumentException("The end address of a range cannot be lower than the start address.", paramName); + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Net/IPAddressRangeExtensions.cs b/Vendor/Swan.Lite-3.1.0/Net/IPAddressRangeExtensions.cs new file mode 100644 index 0000000..44d75ba --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Net/IPAddressRangeExtensions.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Runtime.CompilerServices; + +namespace Swan.Net +{ + /// + /// Provides extension methods for instances and collections of . + /// + public static class IPAddressRangeExtensions + { + /// + /// Determines whether any element of a sequence of instances + /// contains the given . + /// + /// The IEnumerable<IPAddressRange> interface + /// on which this method is called. + /// The to look for. + /// if any of the ranges in + /// contains ; otherwise, . + /// is . + public static bool AnyContains(this IEnumerable @this, IPAddress address) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + foreach (var range in @this) + { + if (range.Contains(address)) + return true; + } + + return false; + } + + /// + /// Determines whether no element of a sequence of instances + /// contains the given . + /// + /// The IEnumerable<IPAddressRange> interface + /// on which this method is called. + /// The to look for. + /// if none of the ranges in + /// contains ; otherwise, . + /// is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool NoneContains(this IEnumerable @this, IPAddress address) + => !AnyContains(@this, address); + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Net/IPAddressUtility.cs b/Vendor/Swan.Lite-3.1.0/Net/IPAddressUtility.cs new file mode 100644 index 0000000..4385c25 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Net/IPAddressUtility.cs @@ -0,0 +1,43 @@ +using System.Net; + +namespace Swan.Net +{ + /// + /// Provides utility methods to work with the class. + /// + public static class IPAddressUtility + { + /// + /// Tries to convert the string representation of an IP address + /// into an instance of + /// This method works the same way as , + /// with the exception that it will not recognize a decimal number alone + /// as an IPv4 address. + /// + /// The string to be converted. + /// When this method returns , + /// an instance of representing the same address + /// as . + /// if the conversion was successful; + /// otherwise, . + public static bool TryParse(string? str, out IPAddress address) + { + // https://docs.microsoft.com/en-us/dotnet/api/system.net.ipaddress.tryparse + // "Note that this method accepts as valid an ipString value that can be parsed as an Int64, + // and then treats that Int64 as the long value of an IP address in network byte order, + // similar to the way that the IPAddress constructor does. + // This means that this method returns true if the Int64 is parsed successfully, + // even if it represents an address that's not a valid IP address. + // For example, if str is "1", this method returns true even though "1" (or 0.0.0.1) + // is not a valid IP address and you might expect this method to return false. + // Fixing this bug would break existing apps, so the current behavior will not be changed. + // Your code can avoid this behavior by ensuring that it only uses this method + // to parse IP addresses in dotted-decimal format." + // --- + // Thus, if it parses as an Int64, let's just refuse it. + // One-part IPv4 addresses be darned. + address = IPAddress.None; + return !long.TryParse(str, out _) && IPAddress.TryParse(str, out address); + } + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Net/Internal/IPAddressValue.cs b/Vendor/Swan.Lite-3.1.0/Net/Internal/IPAddressValue.cs new file mode 100644 index 0000000..e4f33b0 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Net/Internal/IPAddressValue.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; + +namespace Swan.Net.Internal +{ + // NOTE TO CONTRIBUTORS: If you decide to use this type + // in any context other than IPAddressRange, please be aware + // that consistency checks were kept to the bare minimum + // for use by IPAddressRange. + // If you add consistency checks, please ensure + // that IPAddressRange still works as intended. + // Add regression tests if needed. + [Serializable] + internal struct IPAddressValue : IEquatable, IComparable + { + public static readonly IPAddressValue MinValue = new IPAddressValue(ulong.MinValue, ulong.MinValue, false); + public static readonly IPAddressValue MaxValue = new IPAddressValue(ulong.MaxValue, ulong.MaxValue, false); + public static readonly IPAddressValue MinIPv4Value = new IPAddressValue(0UL, 0xFFFF00000000UL, true); + public static readonly IPAddressValue MaxIPv4Value = new IPAddressValue(0UL, 0xFFFFFFFFFFFFUL, true); + + private static readonly IReadOnlyList LowBitMasks = BuildLowBitMasks(); + private static readonly IReadOnlyList HighBitMasks = BuildHighBitMasks(); + + private const long V4Mask0 = 0L; + private const long V4Mask1 = 0xFFFF00000000L; + + private readonly ulong _n0; + private readonly ulong _n1; + private readonly bool _isV4; + + public IPAddressValue(IPAddress address) + { + // There are no overloads of IPAddress.NetworkToHostOrder for unsigned types; + // hence the unchecked casts to signed types. + static ulong ToHostUInt32(byte[] bytes, int startIndex) + => unchecked((uint)IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, startIndex))); + + static ulong ToHostUInt64(byte[] bytes, int startIndex) + => unchecked((ulong)IPAddress.NetworkToHostOrder(BitConverter.ToInt64(bytes, startIndex))); + + if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + var addressBytes = address.GetAddressBytes(); + _n0 = ToHostUInt64(addressBytes, 0); + _n1 = ToHostUInt64(addressBytes, 8); + _isV4 = false; + } + else + { + _n0 = V4Mask0; + _n1 = V4Mask1 + ToHostUInt32(address.GetAddressBytes(), 0); + _isV4 = true; + } + } + + private IPAddressValue(ulong n0, ulong n1, bool isV4) + { + _n0 = n0; + _n1 = n1; + _isV4 = isV4; + } + + // There are no overloads of IPAddress.HostToNetworkOrder for unsigned types; + // hence the unchecked casts to signed types. + public IPAddress ToIPAddress(bool forceV6) + => new IPAddress(_isV4 && !forceV6 + ? BitConverter.GetBytes(IPAddress.HostToNetworkOrder(unchecked((int)(uint)_n1))) + : BitConverter.GetBytes(IPAddress.HostToNetworkOrder(unchecked((long) _n0))) + .Concat(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(unchecked((long) _n1)))) + .ToArray()); + + public override int GetHashCode() => CompositeHashCode.Using(_n0, _n1, _isV4); + + public override bool Equals(object obj) + => obj is IPAddressValue other && Equals(other); + + public bool Equals(IPAddressValue other) + => other._n0 == _n0 + && other._n1 == _n1; + + public int CompareTo(IPAddressValue other) + { + var result = _n0.CompareTo(other._n0); + return result == 0 ?_n1.CompareTo(other._n1) : result; + } + + public bool IsStartOfSubnet(byte prefixLength) + { + var maxPrefixLength = _isV4 ? 32 : 128; + if (prefixLength > maxPrefixLength) + throw SelfCheck.Failure($"Invalid prefix length {prefixLength} in {nameof(IsStartOfSubnet)}"); + + if (_isV4) + prefixLength += 96; + + var bitsToCheck = 128 - prefixLength; + return bitsToCheck < 64 + ? (_n1 & LowBitMasks[bitsToCheck]) == 0 + : _n1 == 0 && (_n0 & LowBitMasks[bitsToCheck]) == 0; + } + + public IPAddressValue GetEndOfSubnet(byte prefixLength) + { + var maxPrefixLength = _isV4 ? 32 : 128; + if (prefixLength > maxPrefixLength) + throw SelfCheck.Failure($"Invalid prefix length {prefixLength} in {nameof(GetEndOfSubnet)}"); + + if (_isV4) + prefixLength += 96; + + var (n0, n1) = prefixLength > 64 + ? (_n0, (_n1 & HighBitMasks[prefixLength - 64]) | LowBitMasks[128 - prefixLength]) + : ((_n0 & HighBitMasks[prefixLength]) | LowBitMasks[64 - prefixLength], 0xFFFFFFFFFFFFFFFFUL); + + return new IPAddressValue(n0, n1, _isV4); + } + + private static IReadOnlyList BuildLowBitMasks() + { + var masks = new ulong[65]; + for (var i = 0; i < 64; i++) + masks[i + 1] = (masks[i] << 1) | 1; + + return masks; + } + + private static IReadOnlyList BuildHighBitMasks() + { + var masks = new ulong[65]; + for (var i = 0; i < 64; i++) + masks[i + 1] = (masks[i] >> 1) | 0x8000000000000000UL; + + return masks; + } + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/ObjectComparer.cs b/Vendor/Swan.Lite-3.1.0/ObjectComparer.cs new file mode 100644 index 0000000..3693044 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/ObjectComparer.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Swan.Reflection; + +namespace Swan +{ + /// + /// Represents a quick object comparer using the public properties of an object + /// or the public members in a structure. + /// + public static class ObjectComparer + { + /// + /// Compare if two variables of the same type are equal. + /// + /// The type of objects to compare. + /// The left. + /// The right. + /// true if the variables are equal; otherwise, false. + public static bool AreEqual(T left, T right) => AreEqual(left, right, typeof(T)); + + /// + /// Compare if two variables of the same type are equal. + /// + /// The left. + /// The right. + /// Type of the target. + /// + /// true if the variables are equal; otherwise, false. + /// + /// targetType. + public static bool AreEqual(object left, object right, Type targetType) + { + if (targetType == null) + throw new ArgumentNullException(nameof(targetType)); + + if (Definitions.BasicTypesInfo.Value.ContainsKey(targetType)) + return Equals(left, right); + + return targetType.IsValueType || targetType.IsArray + ? AreStructsEqual(left, right, targetType) + : AreObjectsEqual(left, right, targetType); + } + + /// + /// Compare if two objects of the same type are equal. + /// + /// The type of objects to compare. + /// The left. + /// The right. + /// true if the objects are equal; otherwise, false. + public static bool AreObjectsEqual(T left, T right) + where T : class + { + return AreObjectsEqual(left, right, typeof(T)); + } + + /// + /// Compare if two objects of the same type are equal. + /// + /// The left. + /// The right. + /// Type of the target. + /// true if the objects are equal; otherwise, false. + /// targetType. + public static bool AreObjectsEqual(object left, object right, Type targetType) + { + if (targetType == null) + throw new ArgumentNullException(nameof(targetType)); + + var properties = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(targetType).ToArray(); + + foreach (var propertyTarget in properties) + { + if (propertyTarget.PropertyType.IsArray) + { + var leftObj = left.ReadProperty(propertyTarget.Name) as IEnumerable; + var rightObj = right.ReadProperty(propertyTarget.Name) as IEnumerable; + + if (!AreEnumerationsEquals(leftObj, rightObj)) + return false; + } + else + { + if (!Equals(left.ReadProperty(propertyTarget.Name), right.ReadProperty(propertyTarget.Name))) + return false; + } + } + + return true; + } + + /// + /// Compare if two structures of the same type are equal. + /// + /// The type of structs to compare. + /// The left. + /// The right. + /// true if the structs are equal; otherwise, false. + public static bool AreStructsEqual(T left, T right) + where T : struct + { + return AreStructsEqual(left, right, typeof(T)); + } + + /// + /// Compare if two structures of the same type are equal. + /// + /// The left. + /// The right. + /// Type of the target. + /// + /// true if the structs are equal; otherwise, false. + /// + /// targetType. + public static bool AreStructsEqual(object left, object right, Type targetType) + { + if (targetType == null) + throw new ArgumentNullException(nameof(targetType)); + + var fields = new List(FieldTypeCache.DefaultCache.Value.RetrieveAllFields(targetType)) + .Union(PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(targetType)); + + foreach (var targetMember in fields) + { + switch (targetMember) + { + case FieldInfo field: + if (!Equals(field.GetValue(left), field.GetValue(right))) + return false; + break; + case PropertyInfo property: + if (!Equals(left.ReadProperty(property.Name), right.ReadProperty(property.Name))) + return false; + break; + } + } + + return true; + } + + /// + /// Compare if two enumerables are equal. + /// + /// The type of enums to compare. + /// The left. + /// The right. + /// + /// true if two specified types are equal; otherwise, false. + /// + /// + /// left + /// or + /// right. + /// + public static bool AreEnumerationsEquals(T left, T right) + where T : IEnumerable? + { + if (Equals(left, default(T))) + throw new ArgumentNullException(nameof(left)); + + if (Equals(right, default(T))) + throw new ArgumentNullException(nameof(right)); + + var leftEnumerable = left.Cast().ToArray(); + var rightEnumerable = right.Cast().ToArray(); + + if (leftEnumerable.Length != rightEnumerable.Length) + return false; + + for (var i = 0; i < leftEnumerable.Length; i++) + { + var leftEl = leftEnumerable[i]; + var rightEl = rightEnumerable[i]; + + if (!AreEqual(leftEl, rightEl, leftEl.GetType())) + { + return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Paginator.cs b/Vendor/Swan.Lite-3.1.0/Paginator.cs new file mode 100644 index 0000000..7be3e43 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Paginator.cs @@ -0,0 +1,99 @@ +using System; + +namespace Swan +{ + /// + /// A utility class to compute paging or batching offsets. + /// + public class Paginator + { + /// + /// Initializes a new instance of the class. + /// + /// The total count of items to page over. + /// The desired size of individual pages. + public Paginator(int totalCount, int pageSize) + { + TotalCount = totalCount; + PageSize = pageSize; + PageCount = ComputePageCount(); + } + + /// + /// Gets the desired number of items per page. + /// + public int PageSize { get; } + + /// + /// Gets the total number of items to page over. + /// + public int TotalCount { get; } + + /// + /// Gets the computed number of pages. + /// + public int PageCount { get; } + + /// + /// Gets the start item index of the given page. + /// + /// Zero-based index of the page. + /// The start item index. + public int GetFirstItemIndex(int pageIndex) + { + pageIndex = FixPageIndex(pageIndex); + return pageIndex * PageSize; + } + + /// + /// Gets the end item index of the given page. + /// + /// Zero-based index of the page. + /// The end item index. + public int GetLastItemIndex(int pageIndex) + { + var startIndex = GetFirstItemIndex(pageIndex); + return Math.Min(startIndex + PageSize - 1, TotalCount - 1); + } + + /// + /// Gets the item count of the given page index. + /// + /// Zero-based index of the page. + /// The number of items that the page contains. + public int GetItemCount(int pageIndex) + { + pageIndex = FixPageIndex(pageIndex); + return (pageIndex >= PageCount - 1) + ? GetLastItemIndex(pageIndex) - GetFirstItemIndex(pageIndex) + 1 + : PageSize; + } + + /// + /// Fixes the index of the page by applying bound logic. + /// + /// Index of the page. + /// A limit-bound index. + private int FixPageIndex(int pageIndex) + { + if (pageIndex < 0) return 0; + + return pageIndex >= PageCount ? PageCount - 1 : pageIndex; + } + + /// + /// Computes the number of pages for the paginator. + /// + /// The page count. + private int ComputePageCount() + { + // include this if when you always want at least 1 page + if (TotalCount == 0) + return 0; + + return TotalCount % PageSize != 0 + ? (TotalCount / PageSize) + 1 + : TotalCount / PageSize; + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Parsers/ArgumentOptionAttribute.cs b/Vendor/Swan.Lite-3.1.0/Parsers/ArgumentOptionAttribute.cs new file mode 100644 index 0000000..ee3f7e2 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Parsers/ArgumentOptionAttribute.cs @@ -0,0 +1,101 @@ +using System; + +namespace Swan.Parsers +{ + /// + /// Models an option specification. + /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors.). + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class ArgumentOptionAttribute + : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The long name of the option. + public ArgumentOptionAttribute(string longName) + : this(string.Empty, longName) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The short name of the option. + /// The long name of the option or null if not used. + public ArgumentOptionAttribute(char shortName, string longName) + : this(new string(shortName, 1), longName) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The short name of the option.. + public ArgumentOptionAttribute(char shortName) + : this(new string(shortName, 1), string.Empty) + { + } + + private ArgumentOptionAttribute(string shortName, string longName) + { + ShortName = shortName ?? throw new ArgumentNullException(nameof(shortName)); + LongName = longName ?? throw new ArgumentNullException(nameof(longName)); + } + + /// + /// Gets long name of this command line option. This name is usually a single English word. + /// + /// + /// The long name. + /// + public string LongName { get; } + + /// + /// Gets a short name of this command line option, made of one character. + /// + /// + /// The short name. + /// + public string ShortName { get; } + + /// + /// When applying attribute to target properties, + /// it allows you to split an argument and consume its content as a sequence. + /// + public char Separator { get; set; } = '\0'; + + /// + /// Gets or sets mapped property default value. + /// + /// + /// The default value. + /// + public object? DefaultValue { get; set; } + + /// + /// Gets or sets a value indicating whether a command line option is required. + /// + /// + /// true if required; otherwise, false. + /// + public bool Required { get; set; } + + /// + /// Gets or sets a short description of this command line option. Usually a sentence summary. + /// + /// + /// The help text. + /// + public string? HelpText { get; set; } + + /// + /// Gets or sets the default argument. + /// + /// + /// The default argument. + /// + public bool IsDefault { get; set; } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Parsers/ArgumentParse.Validator.cs b/Vendor/Swan.Lite-3.1.0/Parsers/ArgumentParse.Validator.cs new file mode 100644 index 0000000..c1dc208 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Parsers/ArgumentParse.Validator.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Swan.Reflection; + +namespace Swan.Parsers +{ + /// + /// Provides methods to parse command line arguments. + /// + /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors). + /// + public partial class ArgumentParser + { + private sealed class Validator + { + private const char OptionSwitchChar = '-'; + private readonly object _instance; + private readonly IEnumerable _args; + private readonly List _updatedList = new List(); + private readonly ArgumentParserSettings _settings; + + private readonly PropertyInfo[] _properties; + + public Validator( + PropertyInfo[] properties, + IEnumerable args, + object instance, + ArgumentParserSettings settings, + bool hasVerb = false) + { + _args = args; + _instance = instance; + _settings = settings; + _properties = properties; + + PopulateInstance(); + if (!hasVerb) SetDefaultArgument(); + SetDefaultValues(); + GetRequiredList(); + } + + public List UnknownList { get; } = new List(); + public List RequiredList { get; } = new List(); + + public bool IsValid() => (_settings.IgnoreUnknownArguments || !UnknownList.Any()) && !RequiredList.Any(); + + public IEnumerable GetPropertiesOptions() + => _properties.Select(p => AttributeCache.DefaultCache.Value.RetrieveOne(p)) + .Where(x => x != null); + + private void GetRequiredList() + { + foreach (var targetProperty in _properties) + { + var optionAttr = AttributeCache.DefaultCache.Value.RetrieveOne(targetProperty); + + if (optionAttr == null || optionAttr.Required == false) + continue; + + if (targetProperty.GetValue(_instance) == null) + { + RequiredList.Add(optionAttr.LongName ?? optionAttr.ShortName); + } + } + } + + private void SetDefaultValues() + { + foreach (var targetProperty in _properties.Except(_updatedList)) + { + var optionAttr = AttributeCache.DefaultCache.Value.RetrieveOne(targetProperty); + + var defaultValue = optionAttr?.DefaultValue; + + if (defaultValue == null) + continue; + + if (SetPropertyValue(targetProperty, defaultValue.ToString(), _instance, optionAttr)) + _updatedList.Add(targetProperty); + } + } + + private void SetDefaultArgument() + { + foreach (var targetProperty in _properties.Except(_updatedList)) + { + var optionAttr = AttributeCache.DefaultCache.Value.RetrieveOne(targetProperty); + + if (!optionAttr.IsDefault) + continue; + + var defaultArgValue = _args.FirstOrDefault(); + if (string.IsNullOrWhiteSpace(defaultArgValue) || defaultArgValue[0] == OptionSwitchChar) + continue; + + if (SetPropertyValue(targetProperty, defaultArgValue, _instance, optionAttr)) + _updatedList.Add(targetProperty); + } + } + + private void PopulateInstance() + { + var propertyName = string.Empty; + + foreach (var arg in _args) + { + var ignoreSetValue = string.IsNullOrWhiteSpace(propertyName); + + if (ignoreSetValue) + { + if (string.IsNullOrWhiteSpace(arg) || arg[0] != OptionSwitchChar) continue; + + propertyName = arg.Substring(1); + + if (!string.IsNullOrWhiteSpace(propertyName) && propertyName[0] == OptionSwitchChar) + propertyName = propertyName.Substring(1); + } + + var targetProperty = TryGetProperty(propertyName); + + if (targetProperty == null) + { + // Skip if the property is not found + UnknownList.Add(propertyName); + continue; + } + + if (!ignoreSetValue && SetPropertyValue(targetProperty, arg, _instance)) + { + _updatedList.Add(targetProperty); + propertyName = string.Empty; + } + else if (targetProperty.PropertyType == typeof(bool)) + { + // If the arg is a boolean property set it to true. + targetProperty.SetValue(_instance, true); + + _updatedList.Add(targetProperty); + propertyName = string.Empty; + } + } + + if (!string.IsNullOrEmpty(propertyName)) + { + UnknownList.Add(propertyName); + } + } + + private bool SetPropertyValue( + PropertyInfo targetProperty, + string propertyValueString, + object result, + ArgumentOptionAttribute? optionAttr = null) + { + if (!targetProperty.PropertyType.IsEnum) + { + return targetProperty.PropertyType.IsArray + ? targetProperty.TrySetArray(propertyValueString.Split(optionAttr?.Separator ?? ','), result) + : targetProperty.TrySetBasicType(propertyValueString, result); + } + + var parsedValue = Enum.Parse( + targetProperty.PropertyType, + propertyValueString, + _settings.CaseInsensitiveEnumValues); + + targetProperty.SetValue(result, Enum.ToObject(targetProperty.PropertyType, parsedValue)); + + return true; + } + + private PropertyInfo TryGetProperty(string propertyName) + => _properties.FirstOrDefault(p => + string.Equals(AttributeCache.DefaultCache.Value.RetrieveOne(p)?.LongName, propertyName, _settings.NameComparer) || + string.Equals(AttributeCache.DefaultCache.Value.RetrieveOne(p)?.ShortName, propertyName, _settings.NameComparer)); + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Parsers/ArgumentParser.TypeResolver.cs b/Vendor/Swan.Lite-3.1.0/Parsers/ArgumentParser.TypeResolver.cs new file mode 100644 index 0000000..2ca5798 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Parsers/ArgumentParser.TypeResolver.cs @@ -0,0 +1,63 @@ +using System; +using System.Linq; +using System.Reflection; +using Swan.Reflection; + +namespace Swan.Parsers +{ + /// + /// Provides methods to parse command line arguments. + /// + public partial class ArgumentParser + { + private sealed class TypeResolver + { + public bool HasVerb { get; } + + private bool _hasVerb = false; + + private readonly string _selectedVerb; + + private PropertyInfo[]? _properties; + + public TypeResolver(string selectedVerb) + { + _selectedVerb = selectedVerb; + } + + public PropertyInfo[]? Properties => _properties?.Any() == true ? _properties : null; + + public object? GetOptionsObject(T instance) + { + _properties = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(true).ToArray(); + + if (!_properties.Any(x => x.GetCustomAttributes(typeof(VerbOptionAttribute), false).Any())) + return instance; + + _hasVerb = true; + + var selectedVerb = string.IsNullOrWhiteSpace(_selectedVerb) + ? null + : _properties.FirstOrDefault(x => + AttributeCache.DefaultCache.Value.RetrieveOne(x).Name == _selectedVerb); + + if (selectedVerb == null) return null; + + var type = instance.GetType(); + + var verbProperty = type.GetProperty(selectedVerb.Name); + + if (verbProperty?.GetValue(instance) == null) + { + var propertyInstance = Activator.CreateInstance(selectedVerb.PropertyType); + verbProperty?.SetValue(instance, propertyInstance); + } + + _properties = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(selectedVerb.PropertyType, true) + .ToArray(); + + return verbProperty?.GetValue(instance); + } + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Parsers/ArgumentParser.cs b/Vendor/Swan.Lite-3.1.0/Parsers/ArgumentParser.cs new file mode 100644 index 0000000..2bfc5b8 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Parsers/ArgumentParser.cs @@ -0,0 +1,253 @@ +using Swan.Reflection; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Swan.Parsers +{ + /// + /// Provides methods to parse command line arguments. + /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors.). + /// + /// + /// The following example shows how to parse CLI arguments into objects. + /// + /// class Example + /// { + /// using System; + /// using Swan.Parsers; + /// + /// static void Main(string[] args) + /// { + /// // parse the supplied command-line arguments into the options object + /// var res = Runtime.ArgumentParser.ParseArguments(args, out var options); + /// } + /// + /// class Options + /// { + /// [ArgumentOption('v', "verbose", HelpText = "Set verbose mode.")] + /// public bool Verbose { get; set; } + /// + /// [ArgumentOption('u', Required = true, HelpText = "Set user name.")] + /// public string Username { get; set; } + /// + /// [ArgumentOption('n', "names", Separator = ',', + /// Required = true, HelpText = "A list of files separated by a comma")] + /// public string[] Files { get; set; } + /// + /// [ArgumentOption('p', "port", DefaultValue = 22, HelpText = "Set port.")] + /// public int Port { get; set; } + /// + /// [ArgumentOption("color", DefaultValue = ConsoleColor.Red, + /// HelpText = "Set a color.")] + /// public ConsoleColor Color { get; set; } + /// } + /// } + /// + /// The following code describes how to parse CLI verbs. + /// + /// class Example2 + /// { + /// using Swan; + /// using Swan.Parsers; + /// + /// static void Main(string[] args) + /// { + /// // create an instance of the VerbOptions class + /// var options = new VerbOptions(); + /// + /// // parse the supplied command-line arguments into the options object + /// var res = Runtime.ArgumentParser.ParseArguments(args, options); + /// + /// // if there were no errors parsing + /// if (res) + /// { + /// if(options.Run != null) + /// { + /// // run verb was selected + /// } + /// + /// if(options.Print != null) + /// { + /// // print verb was selected + /// } + /// } + /// + /// // flush all error messages + /// Terminal.Flush(); + /// } + /// + /// class VerbOptions + /// { + /// [VerbOption("run", HelpText = "Run verb.")] + /// public RunVerbOption Run { get; set; } + /// + /// [VerbOption("print", HelpText = "Print verb.")] + /// public PrintVerbOption Print { get; set; } + /// } + /// + /// class RunVerbOption + /// { + /// [ArgumentOption('o', "outdir", HelpText = "Output directory", + /// DefaultValue = "", Required = false)] + /// public string OutDir { get; set; } + /// } + /// + /// class PrintVerbOption + /// { + /// [ArgumentOption('t', "text", HelpText = "Text to print", + /// DefaultValue = "", Required = false)] + /// public string Text { get; set; } + /// } + /// } + /// + /// + public partial class ArgumentParser + { + /// + /// Initializes a new instance of the class. + /// + public ArgumentParser() + : this(new ArgumentParserSettings()) + { + } + + /// + /// Initializes a new instance of the class, + /// configurable with using a delegate. + /// + /// The parse settings. + public ArgumentParser(ArgumentParserSettings parseSettings) + { + Settings = parseSettings ?? throw new ArgumentNullException(nameof(parseSettings)); + } + + /// + /// Gets the current. + /// + /// + /// The current. + /// + public static ArgumentParser Current { get; } = new ArgumentParser(); + + /// + /// Gets the instance that implements in use. + /// + /// + /// The settings. + /// + public ArgumentParserSettings Settings { get; } + + /// + /// Parses a string array of command line arguments constructing values in an instance of type . + /// + /// The type of the options. + /// The arguments. + /// The instance. + /// + /// true if was converted successfully; otherwise, false. + /// + /// + /// The exception that is thrown when a null reference (Nothing in Visual Basic) + /// is passed to a method that does not accept it as a valid argument. + /// + /// + /// The exception that is thrown when a method call is invalid for the object's current state. + /// + public bool ParseArguments(IEnumerable args, out T instance) + { + instance = Activator.CreateInstance(); + return ParseArguments(args, instance); + } + + /// + /// Parses a string array of command line arguments constructing values in an instance of type . + /// + /// The type of the options. + /// The arguments. + /// The instance. + /// + /// true if was converted successfully; otherwise, false. + /// + /// + /// The exception that is thrown when a null reference (Nothing in Visual Basic) + /// is passed to a method that does not accept it as a valid argument. + /// + /// + /// The exception that is thrown when a method call is invalid for the object's current state. + /// + public bool ParseArguments(IEnumerable args, T instance) + { + if (args == null) + throw new ArgumentNullException(nameof(args)); + + if (Equals(instance, default(T))) + throw new ArgumentNullException(nameof(instance)); + + var typeResolver = new TypeResolver(args.FirstOrDefault()); + var options = typeResolver.GetOptionsObject(instance); + + if (options == null) + { + ReportUnknownVerb(); + return false; + } + + if (typeResolver.Properties == null) + throw new InvalidOperationException($"Type {typeof(T).Name} is not valid"); + + var validator = new Validator(typeResolver.Properties, args, options, Settings, typeResolver.HasVerb); + + if (validator.IsValid()) + return true; + + ReportIssues(validator); + return false; + } + + private static void ReportUnknownVerb() + { + Terminal.WriteLine("No verb was specified", ConsoleColor.Red); + Terminal.WriteLine("Valid verbs:", ConsoleColor.Cyan); + + PropertyTypeCache.DefaultCache.Value + .RetrieveAllProperties(true) + .Select(x => AttributeCache.DefaultCache.Value.RetrieveOne(x)) + .Where(x => x != null) + .ToList() + .ForEach(x => Terminal.WriteLine(x.ToString(), ConsoleColor.Cyan)); + } + + private void ReportIssues(Validator validator) + { + if (Settings.WriteBanner) + Terminal.WriteWelcomeBanner(); + + var options = validator.GetPropertiesOptions(); + + foreach (var option in options) + { + Terminal.WriteLine(string.Empty); + + // TODO: If Enum list values + var shortName = string.IsNullOrWhiteSpace(option.ShortName) ? string.Empty : $"-{option.ShortName}"; + var longName = string.IsNullOrWhiteSpace(option.LongName) ? string.Empty : $"--{option.LongName}"; + var comma = string.IsNullOrWhiteSpace(shortName) || string.IsNullOrWhiteSpace(longName) + ? string.Empty + : ", "; + var defaultValue = option.DefaultValue == null ? string.Empty : $"(Default: {option.DefaultValue}) "; + + Terminal.WriteLine($" {shortName}{comma}{longName}\t\t{defaultValue}{option.HelpText}", ConsoleColor.Cyan); + } + + Terminal.WriteLine(string.Empty); + Terminal.WriteLine(" --help\t\tDisplay this help screen.", ConsoleColor.Cyan); + + if (validator.UnknownList.Any()) + Terminal.WriteLine($"Unknown arguments: {string.Join(", ", validator.UnknownList)}", ConsoleColor.Red); + + if (validator.RequiredList.Any()) + Terminal.WriteLine($"Required arguments: {string.Join(", ", validator.RequiredList)}", ConsoleColor.Red); + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Parsers/ArgumentParserSettings.cs b/Vendor/Swan.Lite-3.1.0/Parsers/ArgumentParserSettings.cs new file mode 100644 index 0000000..dbe2f5b --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Parsers/ArgumentParserSettings.cs @@ -0,0 +1,53 @@ +using System; + +namespace Swan.Parsers +{ + /// + /// Provides settings for . + /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors.). + /// + public class ArgumentParserSettings + { + /// + /// Gets or sets a value indicating whether [write banner]. + /// + /// + /// true if [write banner]; otherwise, false. + /// + public bool WriteBanner { get; set; } = true; + + /// + /// Gets or sets a value indicating whether perform case sensitive comparisons. + /// Note that case insensitivity only applies to parameters, not the values + /// assigned to them (for example, enum parsing). + /// + /// + /// true if [case sensitive]; otherwise, false. + /// + public bool CaseSensitive { get; set; } = false; + + /// + /// Gets or sets a value indicating whether perform case sensitive comparisons of values. + /// Note that case insensitivity only applies to values, not the parameters. + /// + /// + /// true if [case insensitive enum values]; otherwise, false. + /// + public bool CaseInsensitiveEnumValues { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the parser shall move on to the next argument and ignore the given argument if it + /// encounter an unknown arguments. + /// + /// + /// true to allow parsing the arguments with different class options that do not have all the arguments. + /// + /// + /// This allows fragmented version class parsing, useful for project with add-on where add-ons also requires command line arguments but + /// when these are unknown by the main program at build time. + /// + public bool IgnoreUnknownArguments { get; set; } = true; + + internal StringComparison NameComparer => CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Parsers/ExpressionParser.cs b/Vendor/Swan.Lite-3.1.0/Parsers/ExpressionParser.cs new file mode 100644 index 0000000..c189e70 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Parsers/ExpressionParser.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace Swan.Parsers +{ + /// + /// Represents a generic expression parser. + /// + public abstract class ExpressionParser + { + /// + /// Resolves the expression. + /// + /// The type of expression result. + /// The tokens. + /// The representation of the expression parsed. + public virtual T ResolveExpression(IEnumerable tokens) => + ResolveExpression(tokens, System.Globalization.CultureInfo.InvariantCulture); + + /// + /// Resolves the expression. + /// + /// The type of expression result. + /// The tokens. + /// The format provider. + /// The representation of the expression parsed. + public virtual T ResolveExpression(IEnumerable tokens, IFormatProvider formatProvider) + { + var conversion = Expression.Convert(Parse(tokens,formatProvider), typeof(T)); + return Expression.Lambda>(conversion).Compile()(); + } + + /// + /// Parses the specified tokens. + /// + /// The tokens. + /// + /// The final expression. + /// + public virtual Expression Parse(IEnumerable tokens) => + Parse(tokens, System.Globalization.CultureInfo.InvariantCulture); + + /// + /// Parses the specified tokens. + /// + /// The tokens. + /// The format provider. + /// + /// The final expression. + /// + public virtual Expression Parse(IEnumerable tokens, IFormatProvider formatProvider) + { + var expressionStack = new List>(); + + foreach (var token in tokens) + { + if (expressionStack.Any() == false) + expressionStack.Add(new Stack()); + + switch (token.Type) + { + case TokenType.Wall: + expressionStack.Add(new Stack()); + break; + case TokenType.Number: + expressionStack.Last().Push(Expression.Constant(Convert.ToDecimal(token.Value, formatProvider))); + break; + case TokenType.Variable: + ResolveVariable(token.Value, expressionStack.Last()); + break; + case TokenType.String: + expressionStack.Last().Push(Expression.Constant(token.Value)); + break; + case TokenType.Operator: + ResolveOperator(token.Value, expressionStack.Last()); + break; + case TokenType.Function: + ResolveFunction(token.Value, expressionStack.Last()); + + if (expressionStack.Count > 1 && expressionStack.Last().Count == 1) + { + var lastValue = expressionStack.Last().Pop(); + expressionStack.Remove(expressionStack.Last()); + expressionStack.Last().Push(lastValue); + } + + break; + } + } + + return expressionStack.Last().Pop(); + } + + /// + /// Resolves the variable. + /// + /// The value. + /// The expression stack. + public abstract void ResolveVariable(string value, Stack expressionStack); + + /// + /// Resolves the operator. + /// + /// The value. + /// The expression stack. + public abstract void ResolveOperator(string value, Stack expressionStack); + + /// + /// Resolves the function. + /// + /// The value. + /// The expression stack. + public abstract void ResolveFunction(string value, Stack expressionStack); + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Parsers/Operator.cs b/Vendor/Swan.Lite-3.1.0/Parsers/Operator.cs new file mode 100644 index 0000000..598645a --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Parsers/Operator.cs @@ -0,0 +1,32 @@ +namespace Swan.Parsers +{ + /// + /// Represents an operator with precedence. + /// + public class Operator + { + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// + public string Name { get; set; } + + /// + /// Gets or sets the precedence. + /// + /// + /// The precedence. + /// + public int Precedence { get; set; } + + /// + /// Gets or sets a value indicating whether [right associative]. + /// + /// + /// true if [right associative]; otherwise, false. + /// + public bool RightAssociative { get; set; } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Parsers/Token.cs b/Vendor/Swan.Lite-3.1.0/Parsers/Token.cs new file mode 100644 index 0000000..20d30b8 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Parsers/Token.cs @@ -0,0 +1,35 @@ +namespace Swan.Parsers +{ + /// + /// Represents a Token structure. + /// + public struct Token + { + /// + /// Initializes a new instance of the struct. + /// + /// The type. + /// The value. + public Token(TokenType type, string value) + { + Type = type; + Value = type == TokenType.Function || type == TokenType.Operator ? value.ToLowerInvariant() : value; + } + + /// + /// Gets or sets the type. + /// + /// + /// The type. + /// + public TokenType Type { get; set; } + + /// + /// Gets the value. + /// + /// + /// The value. + /// + public string Value { get; } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Parsers/TokenType.cs b/Vendor/Swan.Lite-3.1.0/Parsers/TokenType.cs new file mode 100644 index 0000000..5777f0d --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Parsers/TokenType.cs @@ -0,0 +1,48 @@ +namespace Swan.Parsers +{ + /// + /// Enums the token types. + /// + public enum TokenType + { + /// + /// The number + /// + Number, + + /// + /// The string + /// + String, + + /// + /// The variable + /// + Variable, + + /// + /// The function + /// + Function, + + /// + /// The parenthesis + /// + Parenthesis, + + /// + /// The operator + /// + Operator, + + /// + /// The comma + /// + Comma, + + /// + /// The wall, used to specified the end of argument list of the following function + /// + Wall, + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Parsers/Tokenizer.cs b/Vendor/Swan.Lite-3.1.0/Parsers/Tokenizer.cs new file mode 100644 index 0000000..4d56714 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Parsers/Tokenizer.cs @@ -0,0 +1,361 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Swan.Parsers +{ + /// + /// Represents a generic tokenizer. + /// + public abstract class Tokenizer + { + private const char PeriodChar = '.'; + private const char CommaChar = ','; + private const char StringQuotedChar = '"'; + private const char OpenFuncChar = '('; + private const char CloseFuncChar = ')'; + private const char NegativeChar = '-'; + + private const string OpenFuncStr = "("; + + private readonly List _operators = new List(); + + /// + /// Initializes a new instance of the class. + /// This constructor will use the following default operators: + /// + /// + /// + /// Operator + /// Precedence + /// + /// + /// = + /// 1 + /// + /// + /// != + /// 1 + /// + /// + /// > + /// 2 + /// + /// + /// < + /// 2 + /// + /// + /// >= + /// 2 + /// + /// + /// <= + /// 2 + /// + /// + /// + + /// 3 + /// + /// + /// & + /// 3 + /// + /// + /// - + /// 3 + /// + /// + /// * + /// 4 + /// + /// + /// (backslash) + /// 4 + /// + /// + /// / + /// 4 + /// + /// + /// ^ + /// 4 + /// + /// + /// + /// The input. + protected Tokenizer(string input) + { + _operators.AddRange(GetDefaultOperators()); + Tokenize(input); + } + + /// + /// Initializes a new instance of the class. + /// + /// The input. + /// The operators to use. + protected Tokenizer(string input, IEnumerable operators) + { + _operators.AddRange(operators); + Tokenize(input); + } + + /// + /// Gets the tokens. + /// + /// + /// The tokens. + /// + public List Tokens { get; } = new List(); + + /// + /// Validates the input and return the start index for tokenizer. + /// + /// The input. + /// The start index. + /// true if the input is valid, otherwise false. + public abstract bool ValidateInput(string input, out int startIndex); + + /// + /// Resolves the type of the function or member. + /// + /// The input. + /// The token type. + public abstract TokenType ResolveFunctionOrMemberType(string input); + + /// + /// Evaluates the function or member. + /// + /// The input. + /// The position. + /// true if the input is a valid function or variable, otherwise false. + public virtual bool EvaluateFunctionOrMember(string input, int position) => false; + + /// + /// Gets the default operators. + /// + /// An array with the operators to use for the tokenizer. + public virtual Operator[] GetDefaultOperators() => new[] + { + new Operator {Name = "=", Precedence = 1}, + new Operator {Name = "!=", Precedence = 1}, + new Operator {Name = ">", Precedence = 2}, + new Operator {Name = "<", Precedence = 2}, + new Operator {Name = ">=", Precedence = 2}, + new Operator {Name = "<=", Precedence = 2}, + new Operator {Name = "+", Precedence = 3}, + new Operator {Name = "&", Precedence = 3}, + new Operator {Name = "-", Precedence = 3}, + new Operator {Name = "*", Precedence = 4}, + new Operator {Name = "/", Precedence = 4}, + new Operator {Name = "\\", Precedence = 4}, + new Operator {Name = "^", Precedence = 4}, + }; + + /// + /// Shunting the yard. + /// + /// if set to true [include function stopper] (Token type Wall). + /// + /// Enumerable of the token in in. + /// + /// + /// Wrong token + /// or + /// Mismatched parenthesis. + /// + public virtual IEnumerable ShuntingYard(bool includeFunctionStopper = true) + { + var stack = new Stack(); + + foreach (var tok in Tokens) + { + switch (tok.Type) + { + case TokenType.Number: + case TokenType.Variable: + case TokenType.String: + yield return tok; + break; + case TokenType.Function: + stack.Push(tok); + break; + case TokenType.Operator: + while (stack.Any() && stack.Peek().Type == TokenType.Operator && + CompareOperators(tok.Value, stack.Peek().Value)) + yield return stack.Pop(); + + stack.Push(tok); + break; + case TokenType.Comma: + while (stack.Any() && (stack.Peek().Type != TokenType.Comma && + stack.Peek().Type != TokenType.Parenthesis)) + yield return stack.Pop(); + + break; + case TokenType.Parenthesis: + if (tok.Value == OpenFuncStr) + { + if (stack.Any() && stack.Peek().Type == TokenType.Function) + { + if (includeFunctionStopper) + yield return new Token(TokenType.Wall, tok.Value); + } + + stack.Push(tok); + } + else + { + while (stack.Peek().Value != OpenFuncStr) + yield return stack.Pop(); + + stack.Pop(); + + if (stack.Any() && stack.Peek().Type == TokenType.Function) + { + yield return stack.Pop(); + } + } + + break; + default: + throw new InvalidOperationException("Wrong token"); + } + } + + while (stack.Any()) + { + var tok = stack.Pop(); + if (tok.Type == TokenType.Parenthesis) + throw new InvalidOperationException("Mismatched parenthesis"); + + yield return tok; + } + } + + private static bool CompareOperators(Operator op1, Operator op2) => op1.RightAssociative + ? op1.Precedence < op2.Precedence + : op1.Precedence <= op2.Precedence; + + private void Tokenize(string input) + { + if (!ValidateInput(input, out var startIndex)) + { + return; + } + + for (var i = startIndex; i < input.Length; i++) + { + if (char.IsWhiteSpace(input, i)) continue; + + if (input[i] == CommaChar) + { + Tokens.Add(new Token(TokenType.Comma, new string(new[] { input[i] }))); + continue; + } + + if (input[i] == StringQuotedChar) + { + i = ExtractString(input, i); + continue; + } + + if (char.IsLetter(input, i) || EvaluateFunctionOrMember(input, i)) + { + i = ExtractFunctionOrMember(input, i); + + continue; + } + + if (char.IsNumber(input, i) || ( + input[i] == NegativeChar && + ((Tokens.Any() && Tokens.Last().Type != TokenType.Number) || !Tokens.Any()))) + { + i = ExtractNumber(input, i); + continue; + } + + if (input[i] == OpenFuncChar || + input[i] == CloseFuncChar) + { + Tokens.Add(new Token(TokenType.Parenthesis, new string(new[] { input[i] }))); + continue; + } + + i = ExtractOperator(input, i); + } + } + + private int ExtractData( + string input, + int i, + Func tokenTypeEvaluation, + Func evaluation, + int right = 0, + int left = -1) + { + var charCount = 0; + for (var j = i + right; j < input.Length; j++) + { + if (evaluation(input[j])) + break; + + charCount++; + } + + // Extract and set the value + var value = input.SliceLength(i + right, charCount); + Tokens.Add(new Token(tokenTypeEvaluation(value), value)); + + i += charCount + left; + return i; + } + + private int ExtractOperator(string input, int i) => + ExtractData( + input, + i, + x => TokenType.Operator, + x => x == OpenFuncChar || + x == CommaChar || + x == PeriodChar || + x == StringQuotedChar || + char.IsWhiteSpace(x) || + char.IsNumber(x)); + + private int ExtractFunctionOrMember(string input, int i) => + ExtractData( + input, + i, + ResolveFunctionOrMemberType, + x => x == OpenFuncChar || + x == CloseFuncChar || + x == CommaChar || + char.IsWhiteSpace(x)); + + private int ExtractNumber(string input, int i) => + ExtractData( + input, + i, + x => TokenType.Number, + x => !char.IsNumber(x) && x != PeriodChar && x != NegativeChar); + + private int ExtractString(string input, int i) + { + var length = ExtractData(input, i, x => TokenType.String, x => x == StringQuotedChar, 1, 1); + + // open string, report issue + if (length == input.Length && input[length - 1] != StringQuotedChar) + throw new FormatException($"Parser error (Position {i}): Expected '\"' but got '{input[length - 1]}'."); + + return length; + } + + private bool CompareOperators(string op1, string op2) + => CompareOperators(GetOperatorOrDefault(op1), GetOperatorOrDefault(op2)); + + private Operator GetOperatorOrDefault(string op) + => _operators.FirstOrDefault(x => x.Name == op) ?? new Operator { Name = op, Precedence = 0 }; + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Parsers/VerbOptionAttribute.cs b/Vendor/Swan.Lite-3.1.0/Parsers/VerbOptionAttribute.cs new file mode 100644 index 0000000..79496b2 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Parsers/VerbOptionAttribute.cs @@ -0,0 +1,40 @@ +using System; + +namespace Swan.Parsers +{ + /// + /// Models a verb option. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class VerbOptionAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The name. + /// name. + public VerbOptionAttribute(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + /// + /// Gets the name of the verb option. + /// + /// + /// Name. + /// + public string Name { get; } + + /// + /// Gets or sets a short description of this command line verb. Usually a sentence summary. + /// + /// + /// The help text. + /// + public string HelpText { get; set; } + + /// + public override string ToString() => $" {Name}\t\t{HelpText}"; + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Reflection/AttributeCache.cs b/Vendor/Swan.Lite-3.1.0/Reflection/AttributeCache.cs new file mode 100644 index 0000000..c4440af --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Reflection/AttributeCache.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Swan.Reflection +{ + /// + /// A thread-safe cache of attributes belonging to a given key (MemberInfo or Type). + /// + /// The Retrieve method is the most useful one in this class as it + /// calls the retrieval process if the type is not contained + /// in the cache. + /// + public class AttributeCache + { + private readonly Lazy, IEnumerable>> _data = + new Lazy, IEnumerable>>(() => + new ConcurrentDictionary, IEnumerable>(), true); + + /// + /// Initializes a new instance of the class. + /// + /// The property cache object. + public AttributeCache(PropertyTypeCache? propertyCache = null) + { + PropertyTypeCache = propertyCache ?? PropertyTypeCache.DefaultCache.Value; + } + + /// + /// Gets the default cache. + /// + /// + /// The default cache. + /// + public static Lazy DefaultCache { get; } = new Lazy(() => new AttributeCache()); + + /// + /// A PropertyTypeCache object for caching properties and their attributes. + /// + public PropertyTypeCache PropertyTypeCache { get; } + + /// + /// Determines whether [contains] [the specified member]. + /// + /// The type of the attribute to be retrieved. + /// The member. + /// + /// true if [contains] [the specified member]; otherwise, false. + /// + public bool Contains(MemberInfo member) => _data.Value.ContainsKey(new Tuple(member, typeof(T))); + + /// + /// Gets specific attributes from a member constrained to an attribute. + /// + /// The type of the attribute to be retrieved. + /// The member. + /// true to inspect the ancestors of element; otherwise, false. + /// An array of the attributes stored for the specified type. + public IEnumerable Retrieve(MemberInfo member, bool inherit = false) + where T : Attribute + { + if (member == null) + throw new ArgumentNullException(nameof(member)); + + return Retrieve(new Tuple(member, typeof(T)), t => member.GetCustomAttributes(inherit)); + } + + /// + /// Gets all attributes of a specific type from a member. + /// + /// The member. + /// The attribute type. + /// true to inspect the ancestors of element; otherwise, false. + /// An array of the attributes stored for the specified type. + public IEnumerable Retrieve(MemberInfo member, Type type, bool inherit = false) + { + if (member == null) + throw new ArgumentNullException(nameof(member)); + + if (type == null) + throw new ArgumentNullException(nameof(type)); + + return Retrieve( + new Tuple(member, type), + t => member.GetCustomAttributes(type, inherit)); + } + + /// + /// Gets one attribute of a specific type from a member. + /// + /// The attribute type. + /// The member. + /// true to inspect the ancestors of element; otherwise, false. + /// An attribute stored for the specified type. + public T RetrieveOne(MemberInfo member, bool inherit = false) + where T : Attribute + { + if (member == null) + return default; + + var attr = Retrieve( + new Tuple(member, typeof(T)), + t => member.GetCustomAttributes(typeof(T), inherit)); + + return ConvertToAttribute(attr); + } + + /// + /// Gets one attribute of a specific type from a generic type. + /// + /// The type of the attribute. + /// The type to retrieve the attribute. + /// if set to true [inherit]. + /// An attribute stored for the specified type. + public TAttribute RetrieveOne(bool inherit = false) + where TAttribute : Attribute + { + var attr = Retrieve( + new Tuple(typeof(T), typeof(TAttribute)), + t => typeof(T).GetCustomAttributes(typeof(TAttribute), inherit)); + + return ConvertToAttribute(attr); + } + + /// + /// Gets all properties an their attributes of a given type constrained to only attributes. + /// + /// The type of the attribute to retrieve. + /// The type of the object. + /// true to inspect the ancestors of element; otherwise, false. + /// A dictionary of the properties and their attributes stored for the specified type. + public Dictionary> Retrieve(Type type, bool inherit = false) + where T : Attribute => + PropertyTypeCache.RetrieveAllProperties(type, true) + .ToDictionary(x => x, x => Retrieve(x, inherit)); + + /// + /// Gets all properties and their attributes of a given type. + /// + /// The object type used to extract the properties from. + /// The type of the attribute. + /// true to inspect the ancestors of element; otherwise, false. + /// + /// A dictionary of the properties and their attributes stored for the specified type. + /// + public Dictionary> RetrieveFromType(bool inherit = false) + => RetrieveFromType(typeof(TAttribute), inherit); + + /// + /// Gets all properties and their attributes of a given type. + /// + /// The object type used to extract the properties from. + /// Type of the attribute. + /// true to inspect the ancestors of element; otherwise, false. + /// + /// A dictionary of the properties and their attributes stored for the specified type. + /// + public Dictionary> RetrieveFromType(Type attributeType, bool inherit = false) + { + if (attributeType == null) + throw new ArgumentNullException(nameof(attributeType)); + + return PropertyTypeCache.RetrieveAllProperties(true) + .ToDictionary(x => x, x => Retrieve(x, attributeType, inherit)); + } + + private static T ConvertToAttribute(IEnumerable attr) + where T : Attribute + { + if (attr?.Any() != true) + return default; + + return attr.Count() == 1 + ? (T) Convert.ChangeType(attr.First(), typeof(T)) + : throw new AmbiguousMatchException("Multiple custom attributes of the same type found."); + } + + private IEnumerable Retrieve(Tuple key, Func, IEnumerable> factory) + { + if (factory == null) + throw new ArgumentNullException(nameof(factory)); + + return _data.Value.GetOrAdd(key, k => factory.Invoke(k).Where(item => item != null)); + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Reflection/ConstructorTypeCache.cs b/Vendor/Swan.Lite-3.1.0/Reflection/ConstructorTypeCache.cs new file mode 100644 index 0000000..9098e0c --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Reflection/ConstructorTypeCache.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Swan.Reflection +{ + /// + /// A thread-safe cache of constructors belonging to a given type. + /// + public class ConstructorTypeCache : TypeCache> + { + /// + /// Gets the default cache. + /// + /// + /// The default cache. + /// + public static Lazy DefaultCache { get; } = + new Lazy(() => new ConstructorTypeCache()); + + /// + /// Retrieves all constructors order by the number of parameters ascending. + /// + /// The type to inspect. + /// if set to true [include non public]. + /// + /// A collection with all the constructors in the given type. + /// + public IEnumerable> RetrieveAllConstructors(bool includeNonPublic = false) + => Retrieve(GetConstructors(includeNonPublic)); + + /// + /// Retrieves all constructors order by the number of parameters ascending. + /// + /// The type. + /// if set to true [include non public]. + /// + /// A collection with all the constructors in the given type. + /// + public IEnumerable> RetrieveAllConstructors(Type type, bool includeNonPublic = false) + => Retrieve(type, GetConstructors(includeNonPublic)); + + private static Func>> GetConstructors(bool includeNonPublic) + => t => t.GetConstructors(includeNonPublic ? BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance : BindingFlags.Public | BindingFlags.Instance) + .Select(x => Tuple.Create(x, x.GetParameters())) + .OrderBy(x => x.Item2.Length) + .ToList(); + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Reflection/ExtendedTypeInfo.cs b/Vendor/Swan.Lite-3.1.0/Reflection/ExtendedTypeInfo.cs new file mode 100644 index 0000000..fbaed95 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Reflection/ExtendedTypeInfo.cs @@ -0,0 +1,267 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reflection; + +namespace Swan.Reflection +{ + /// + /// Provides extended information about a type. + /// + /// This class is mainly used to define sets of types within the Definition class + /// and it is not meant for other than querying the BasicTypesInfo dictionary. + /// + public class ExtendedTypeInfo + { + private const string TryParseMethodName = nameof(byte.TryParse); + private const string ToStringMethodName = nameof(ToString); + + private static readonly Type[] NumericTypes = + { + typeof(byte), + typeof(sbyte), + typeof(decimal), + typeof(double), + typeof(float), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(short), + typeof(ushort), + }; + + private readonly ParameterInfo[]? _tryParseParameters; + private readonly int _toStringArgumentLength; + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The t. + public ExtendedTypeInfo(Type t) + { + Type = t ?? throw new ArgumentNullException(nameof(t)); + IsNullableValueType = Type.IsGenericType + && Type.GetGenericTypeDefinition() == typeof(Nullable<>); + + IsValueType = t.IsValueType; + + UnderlyingType = IsNullableValueType ? + new NullableConverter(Type).UnderlyingType : + Type; + + IsNumeric = NumericTypes.Contains(UnderlyingType); + + // Extract the TryParse method info + try + { + TryParseMethodInfo = UnderlyingType.GetMethod(TryParseMethodName, + new[] { typeof(string), typeof(NumberStyles), typeof(IFormatProvider), UnderlyingType.MakeByRefType() }) ?? + UnderlyingType.GetMethod(TryParseMethodName, + new[] { typeof(string), UnderlyingType.MakeByRefType() }); + + _tryParseParameters = TryParseMethodInfo?.GetParameters(); + } + catch + { + // ignored + } + + // Extract the ToString method Info + try + { + ToStringMethodInfo = UnderlyingType.GetMethod(ToStringMethodName, + new[] { typeof(IFormatProvider) }) ?? + UnderlyingType.GetMethod(ToStringMethodName, + Array.Empty()); + + _toStringArgumentLength = ToStringMethodInfo?.GetParameters().Length ?? 0; + } + catch + { + // ignored + } + } + + #endregion + + #region Properties + + /// + /// Gets the type this extended info class provides for. + /// + /// + /// The type. + /// + public Type Type { get; } + + /// + /// Gets a value indicating whether the type is a nullable value type. + /// + /// + /// true if this instance is nullable value type; otherwise, false. + /// + public bool IsNullableValueType { get; } + + /// + /// Gets a value indicating whether the type or underlying type is numeric. + /// + /// + /// true if this instance is numeric; otherwise, false. + /// + public bool IsNumeric { get; } + + /// + /// Gets a value indicating whether the type is value type. + /// Nullable value types have this property set to False. + /// + public bool IsValueType { get; } + + /// + /// When dealing with nullable value types, this property will + /// return the underlying value type of the nullable, + /// Otherwise it will return the same type as the Type property. + /// + /// + /// The type of the underlying. + /// + public Type UnderlyingType { get; } + + /// + /// Gets the try parse method information. If the type does not contain + /// a suitable TryParse static method, it will return null. + /// + /// + /// The try parse method information. + /// + public MethodInfo TryParseMethodInfo { get; } + + /// + /// Gets the ToString method info + /// It will prefer the overload containing the IFormatProvider argument. + /// + /// + /// To string method information. + /// + public MethodInfo ToStringMethodInfo { get; } + + /// + /// Gets a value indicating whether the type contains a suitable TryParse method. + /// + /// + /// true if this instance can parse natively; otherwise, false. + /// + public bool CanParseNatively => TryParseMethodInfo != null; + + #endregion + + #region Methods + + /// + /// Tries to parse the string into an object of the type this instance represents. + /// Returns false when no suitable TryParse methods exists for the type or when parsing fails + /// for any reason. When possible, this method uses CultureInfo.InvariantCulture and NumberStyles.Any. + /// + /// The s. + /// The result. + /// true if parse was converted successfully; otherwise, false. + public bool TryParse(string s, out object? result) + { + result = Type.GetDefault(); + + try + { + if (Type == typeof(string)) + { + result = Convert.ChangeType(s, Type, CultureInfo.InvariantCulture); + return true; + } + + if ((IsNullableValueType && string.IsNullOrEmpty(s)) || !CanParseNatively) + { + return true; + } + + // Build the arguments of the TryParse method + var dynamicArguments = new List { s }; + + for (var pi = 1; pi < _tryParseParameters.Length - 1; pi++) + { + var argInfo = _tryParseParameters[pi]; + if (argInfo.ParameterType == typeof(IFormatProvider)) + dynamicArguments.Add(CultureInfo.InvariantCulture); + else if (argInfo.ParameterType == typeof(NumberStyles)) + dynamicArguments.Add(NumberStyles.Any); + else + dynamicArguments.Add(null); + } + + dynamicArguments.Add(null); + var parseArguments = dynamicArguments.ToArray(); + + if ((bool) TryParseMethodInfo.Invoke(null, parseArguments)) + { + result = parseArguments[parseArguments.Length - 1]; + return true; + } + } + catch + { + // Ignore + } + + return false; + } + + /// + /// Converts this instance to its string representation, + /// trying to use the CultureInfo.InvariantCulture + /// IFormat provider if the overload is available. + /// + /// The instance. + /// A that represents the current object. + public string ToStringInvariant(object instance) + { + if (instance == null) + return string.Empty; + + return _toStringArgumentLength != 1 + ? instance.ToString() + : ToStringMethodInfo.Invoke(instance, new object[] {CultureInfo.InvariantCulture}) as string ?? string.Empty; + } + + #endregion + } + + /// + /// Provides extended information about a type. + /// + /// This class is mainly used to define sets of types within the Constants class + /// and it is not meant for other than querying the BasicTypesInfo dictionary. + /// + /// The type of extended type information. + public class ExtendedTypeInfo : ExtendedTypeInfo + { + /// + /// Initializes a new instance of the class. + /// + public ExtendedTypeInfo() + : base(typeof(T)) + { + // placeholder + } + + /// + /// Converts this instance to its string representation, + /// trying to use the CultureInfo.InvariantCulture + /// IFormat provider if the overload is available. + /// + /// The instance. + /// A that represents the current object. + public string ToStringInvariant(T instance) => base.ToStringInvariant(instance); + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Reflection/IPropertyProxy.cs b/Vendor/Swan.Lite-3.1.0/Reflection/IPropertyProxy.cs new file mode 100644 index 0000000..d2f2040 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Reflection/IPropertyProxy.cs @@ -0,0 +1,45 @@ +using System; +using System.Reflection; + +namespace Swan.Reflection +{ + /// + /// Represents a generic interface to store getters and setters for high speed access to properties. + /// + public interface IPropertyProxy + { + /// + /// Gets the name of the property. + /// + string Name { get; } + + /// + /// Gets the type of the property. + /// + Type PropertyType { get; } + + /// + /// Gets the associated reflection property info. + /// + PropertyInfo Property { get; } + + /// + /// Gets the type owning this property proxy. + /// + Type EnclosingType { get; } + + /// + /// Gets the property value via a stored delegate. + /// + /// The instance. + /// The property value. + object? GetValue(object instance); + + /// + /// Sets the property value via a stored delegate. + /// + /// The instance. + /// The value. + void SetValue(object instance, object? value); + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Reflection/MethodInfoCache.cs b/Vendor/Swan.Lite-3.1.0/Reflection/MethodInfoCache.cs new file mode 100644 index 0000000..ca363d0 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Reflection/MethodInfoCache.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Concurrent; +using System.Reflection; + +namespace Swan.Reflection +{ + /// + /// Represents a Method Info Cache. + /// + public class MethodInfoCache : ConcurrentDictionary + { + /// + /// Retrieves the properties stored for the specified type. + /// If the properties are not available, it calls the factory method to retrieve them + /// and returns them as an array of PropertyInfo. + /// + /// The type of type. + /// The name. + /// The alias. + /// The types. + /// + /// The cached MethodInfo. + /// + /// name + /// or + /// factory. + public MethodInfo Retrieve(string name, string alias, params Type[] types) + => Retrieve(typeof(T), name, alias, types); + + /// + /// Retrieves the specified name. + /// + /// The type of type. + /// The name. + /// The types. + /// + /// The cached MethodInfo. + /// + public MethodInfo Retrieve(string name, params Type[] types) + => Retrieve(typeof(T), name, name, types); + + /// + /// Retrieves the specified type. + /// + /// The type. + /// The name. + /// The types. + /// + /// An array of the properties stored for the specified type. + /// + public MethodInfo Retrieve(Type type, string name, params Type[] types) + => Retrieve(type, name, name, types); + + /// + /// Retrieves the specified type. + /// + /// The type. + /// The name. + /// The alias. + /// The types. + /// + /// The cached MethodInfo. + /// + public MethodInfo Retrieve(Type type, string name, string alias, params Type[] types) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (alias == null) + throw new ArgumentNullException(nameof(alias)); + + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return GetOrAdd( + alias, + x => type.GetMethod(name, types ?? Array.Empty())); + } + + /// + /// Retrieves the specified name. + /// + /// The type of type. + /// The name. + /// + /// The cached MethodInfo. + /// + public MethodInfo Retrieve(string name) + => Retrieve(typeof(T), name); + + /// + /// Retrieves the specified type. + /// + /// The type. + /// The name. + /// + /// The cached MethodInfo. + /// + /// + /// type + /// or + /// name. + /// + public MethodInfo Retrieve(Type type, string name) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return GetOrAdd( + name, + type.GetMethod); + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Reflection/PropertyInfoProxy.cs b/Vendor/Swan.Lite-3.1.0/Reflection/PropertyInfoProxy.cs new file mode 100644 index 0000000..466978d --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Reflection/PropertyInfoProxy.cs @@ -0,0 +1,79 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace Swan.Reflection +{ + /// + /// The concrete and hidden implementation of the implementation. + /// + /// + internal sealed class PropertyInfoProxy : IPropertyProxy + { + private readonly Func? _getter; + private readonly Action? _setter; + + /// + /// Initializes a new instance of the class. + /// + /// Type of the declaring. + /// The property information. + public PropertyInfoProxy(Type declaringType, PropertyInfo propertyInfo) + { + Property = propertyInfo; + EnclosingType = declaringType; + _getter = CreateLambdaGetter(declaringType, propertyInfo); + _setter = CreateLambdaSetter(declaringType, propertyInfo); + } + + /// + public PropertyInfo Property { get; } + + /// + public Type EnclosingType { get; } + + /// + public string Name => Property.Name; + + /// + public Type PropertyType => Property.PropertyType; + + /// + public object? GetValue(object instance) => _getter?.Invoke(instance); + + /// + public void SetValue(object instance, object? value) => _setter?.Invoke(instance, value); + + private static Func? CreateLambdaGetter(Type instanceType, PropertyInfo propertyInfo) + { + if (!propertyInfo.CanRead) + return null; + + var instanceParameter = Expression.Parameter(typeof(object), "instance"); + var typedInstance = Expression.Convert(instanceParameter, instanceType); + var property = Expression.Property(typedInstance, propertyInfo); + var convert = Expression.Convert(property, typeof(object)); + var dynamicGetter = (Func)Expression.Lambda(convert, instanceParameter).Compile(); + + return dynamicGetter; + } + + private static Action? CreateLambdaSetter(Type instanceType, PropertyInfo propertyInfo) + { + if (!propertyInfo.CanWrite) + return null; + + var instanceParameter = Expression.Parameter(typeof(object), "instance"); + var valueParameter = Expression.Parameter(typeof(object), "value"); + + var typedInstance = Expression.Convert(instanceParameter, instanceType); + var property = Expression.Property(typedInstance, propertyInfo); + var propertyValue = Expression.Convert(valueParameter, propertyInfo.PropertyType); + + var body = Expression.Assign(property, propertyValue); + var dynamicSetter = Expression.Lambda>(body, instanceParameter, valueParameter).Compile(); + + return dynamicSetter; + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Reflection/PropertyTypeCache.cs b/Vendor/Swan.Lite-3.1.0/Reflection/PropertyTypeCache.cs new file mode 100644 index 0000000..e7f5f9a --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Reflection/PropertyTypeCache.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Swan.Reflection +{ + /// + /// A thread-safe cache of properties belonging to a given type. + /// + public class PropertyTypeCache : TypeCache + { + /// + /// Gets the default cache. + /// + /// + /// The default cache. + /// + public static Lazy DefaultCache { get; } = new Lazy(() => new PropertyTypeCache()); + + /// + /// Retrieves all properties. + /// + /// The type to inspect. + /// if set to true [only public]. + /// + /// A collection with all the properties in the given type. + /// + public IEnumerable RetrieveAllProperties(bool onlyPublic = false) + => Retrieve(onlyPublic ? GetAllPublicPropertiesFunc() : GetAllPropertiesFunc()); + + /// + /// Retrieves all properties. + /// + /// The type. + /// if set to true [only public]. + /// + /// A collection with all the properties in the given type. + /// + public IEnumerable RetrieveAllProperties(Type type, bool onlyPublic = false) + => Retrieve(type, onlyPublic ? GetAllPublicPropertiesFunc() : GetAllPropertiesFunc()); + + /// + /// Retrieves the filtered properties. + /// + /// The type. + /// if set to true [only public]. + /// The filter. + /// + /// A collection with all the properties in the given type. + /// + public IEnumerable RetrieveFilteredProperties( + Type type, + bool onlyPublic, + Func filter) + => Retrieve(type, + onlyPublic ? GetAllPublicPropertiesFunc(filter) : GetAllPropertiesFunc(filter)); + + private static Func> GetAllPropertiesFunc( + Func? filter = null) + => GetPropertiesFunc( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + filter); + + private static Func> GetAllPublicPropertiesFunc( + Func? filter = null) + => GetPropertiesFunc(BindingFlags.Public | BindingFlags.Instance, filter); + + private static Func> GetPropertiesFunc(BindingFlags flags, + Func? filter = null) + => t => t.GetProperties(flags) + .Where(filter ?? (p => p.CanRead || p.CanWrite)); + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Reflection/TypeCache.cs b/Vendor/Swan.Lite-3.1.0/Reflection/TypeCache.cs new file mode 100644 index 0000000..48ea8cd --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Reflection/TypeCache.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Swan.Collections; + +namespace Swan.Reflection +{ + /// + /// A thread-safe cache of members belonging to a given type. + /// + /// The Retrieve method is the most useful one in this class as it + /// calls the retrieval process if the type is not contained + /// in the cache. + /// + /// The type of Member to be cached. + public abstract class TypeCache : CollectionCacheRepository + { + /// + /// Determines whether the cache contains the specified type. + /// + /// The type of the out. + /// + /// true if [contains]; otherwise, false. + /// + public bool Contains() => ContainsKey(typeof(TOut)); + + /// + /// Retrieves the properties stored for the specified type. + /// If the properties are not available, it calls the factory method to retrieve them + /// and returns them as an array of PropertyInfo. + /// + /// The type of the out. + /// The factory. + /// An array of the properties stored for the specified type. + public IEnumerable Retrieve(Func> factory) + => Retrieve(typeof(TOut), factory); + } + + /// + /// A thread-safe cache of fields belonging to a given type + /// The Retrieve method is the most useful one in this class as it + /// calls the retrieval process if the type is not contained + /// in the cache. + /// + public class FieldTypeCache : TypeCache + { + /// + /// Gets the default cache. + /// + /// + /// The default cache. + /// + public static Lazy DefaultCache { get; } = new Lazy(() => new FieldTypeCache()); + + /// + /// Retrieves all fields. + /// + /// The type to inspect. + /// + /// A collection with all the fields in the given type. + /// + public IEnumerable RetrieveAllFields() + => Retrieve(GetAllFieldsFunc()); + + /// + /// Retrieves all fields. + /// + /// The type. + /// + /// A collection with all the fields in the given type. + /// + public IEnumerable RetrieveAllFields(Type type) + => Retrieve(type, GetAllFieldsFunc()); + + private static Func> GetAllFieldsFunc() + => t => t.GetFields(BindingFlags.Public | BindingFlags.Instance); + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/SelfCheck.cs b/Vendor/Swan.Lite-3.1.0/SelfCheck.cs new file mode 100644 index 0000000..497e4c3 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/SelfCheck.cs @@ -0,0 +1,62 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Swan +{ + /// + /// Provides methods to perform self-checks in library or application code. + /// + public static class SelfCheck + { + /// + /// Creates and returns an exception telling that an internal self-check has failed. + /// The returned exception will be of type ; its + /// Message property will contain the specified + /// , preceded by an indication of the assembly, source file, + /// and line number of the failed check. + /// + /// The exception message. + /// The path of the source file where this method is called. + /// This parameter is automatically added by the compiler amd should never be provided explicitly. + /// The line number in source where this method is called. + /// This parameter is automatically added by the compiler amd should never be provided explicitly. + /// + /// A newly-created instance of . + /// + public static InternalErrorException Failure(string message, [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0) + => new InternalErrorException(BuildMessage(message, filePath, lineNumber)); + + private static string BuildMessage(string message, string filePath, int lineNumber) + { + var frames = new StackTrace().GetFrames(); + if (frames == null) + return message; + + try + { + filePath = Path.GetFileName(filePath); + } + catch (ArgumentException) + { + } + + var frame = frames.FirstOrDefault(f => f.GetMethod().ReflectedType != typeof(SelfCheck)); + var sb = new StringBuilder() + .Append('[') + .Append(frame?.GetType().Assembly.GetName().Name ?? ""); + + if (!string.IsNullOrEmpty(filePath)) + { + sb.Append(": ").Append(filePath); + if (lineNumber > 0) + sb.Append('(').Append(lineNumber).Append(')'); + } + + return sb.Append("] ").Append(message).ToString(); + } + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/SingletonBase.cs b/Vendor/Swan.Lite-3.1.0/SingletonBase.cs new file mode 100644 index 0000000..9df0af0 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/SingletonBase.cs @@ -0,0 +1,61 @@ +using System; + +namespace Swan +{ + /// + /// Represents a singleton pattern abstract class. + /// + /// The type of class. + public abstract class SingletonBase : IDisposable + where T : class + { + /// + /// The static, singleton instance reference. + /// + protected static readonly Lazy LazyInstance = new Lazy( + valueFactory: () => Activator.CreateInstance(typeof(T), true) as T, + isThreadSafe: true); + + private bool _isDisposing; // To detect redundant calls + + /// + /// Gets the instance that this singleton represents. + /// If the instance is null, it is constructed and assigned when this member is accessed. + /// + /// + /// The instance. + /// + public static T Instance => LazyInstance.Value; + + /// + public void Dispose() => Dispose(true); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// Call the GC.SuppressFinalize if you override this method and use + /// a non-default class finalizer (destructor). + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposeManaged) + { + if (_isDisposing) return; + + _isDisposing = true; + + // free managed resources + if (LazyInstance == null) return; + + try + { + var disposableInstance = LazyInstance.Value as IDisposable; + disposableInstance?.Dispose(); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // swallow + } + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/StringConversionException.cs b/Vendor/Swan.Lite-3.1.0/StringConversionException.cs new file mode 100644 index 0000000..30f7517 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/StringConversionException.cs @@ -0,0 +1,76 @@ +using System; +using System.Runtime.Serialization; + +namespace Swan +{ + /// + /// The exception that is thrown when a conversion from a string to a + /// specified type fails. + /// + /// + [Serializable] + public class StringConversionException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public StringConversionException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + public StringConversionException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, + /// or if no inner exception is specified. + public StringConversionException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The desired resulting type of the attempted conversion. + public StringConversionException(Type type) + : base(BuildStandardMessageForType(type)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The desired resulting type of the attempted conversion. + /// The exception that is the cause of the current exception, + /// or if no inner exception is specified. + public StringConversionException(Type type, Exception innerException) + : base(BuildStandardMessageForType(type), innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data + /// about the exception being thrown. + /// The that contains contextual information + /// about the source or destination. + protected StringConversionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + private static string BuildStandardMessageForType(Type type) + => $"Cannot convert a string to an instance of {type.FullName}"; + } +} diff --git a/Vendor/Swan.Lite-3.1.0/StructEndiannessAttribute.cs b/Vendor/Swan.Lite-3.1.0/StructEndiannessAttribute.cs new file mode 100644 index 0000000..16b7932 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/StructEndiannessAttribute.cs @@ -0,0 +1,30 @@ +using System; + +namespace Swan +{ + /// + /// An attribute used to help conversion structs back and forth into arrays of bytes via + /// extension methods included in this library ToStruct and ToBytes. + /// + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Struct)] + public class StructEndiannessAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The endianness. + public StructEndiannessAttribute(Endianness endianness) + { + Endianness = endianness; + } + + /// + /// Gets the endianness. + /// + /// + /// The endianness. + /// + public Endianness Endianness { get; } + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/SwanRuntime.cs b/Vendor/Swan.Lite-3.1.0/SwanRuntime.cs new file mode 100644 index 0000000..1ef359d --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/SwanRuntime.cs @@ -0,0 +1,237 @@ +using Swan.Logging; +using System; +using System.IO; +using System.Reflection; +using System.Threading; + +namespace Swan +{ + /// + /// Provides utility methods to retrieve information about the current application. + /// + public static class SwanRuntime + { + private static readonly Lazy EntryAssemblyLazy = new Lazy(Assembly.GetEntryAssembly); + + private static readonly Lazy CompanyNameLazy = new Lazy(() => + { + var attribute = + EntryAssembly.GetCustomAttribute(typeof(AssemblyCompanyAttribute)) as AssemblyCompanyAttribute; + return attribute?.Company ?? string.Empty; + }); + + private static readonly Lazy ProductNameLazy = new Lazy(() => + { + var attribute = + EntryAssembly.GetCustomAttribute(typeof(AssemblyProductAttribute)) as AssemblyProductAttribute; + return attribute?.Product ?? string.Empty; + }); + + private static readonly Lazy ProductTrademarkLazy = new Lazy(() => + { + var attribute = + EntryAssembly.GetCustomAttribute(typeof(AssemblyTrademarkAttribute)) as AssemblyTrademarkAttribute; + return attribute?.Trademark ?? string.Empty; + }); + + private static readonly string ApplicationMutexName = "Global\\{{" + EntryAssembly.FullName + "}}"; + + private static readonly object SyncLock = new object(); + + private static OperatingSystem? _oS; + + #region Properties + + /// + /// Gets the current Operating System. + /// + /// + /// The os. + /// + public static OperatingSystem OS + { + get + { + if (_oS.HasValue == false) + { + var windowsDirectory = Environment.GetEnvironmentVariable("windir"); + if (string.IsNullOrEmpty(windowsDirectory) == false + && windowsDirectory.Contains(@"\") + && Directory.Exists(windowsDirectory)) + { + _oS = OperatingSystem.Windows; + } + else + { + _oS = File.Exists(@"/proc/sys/kernel/ostype") ? OperatingSystem.Unix : OperatingSystem.Osx; + } + } + + return _oS ?? OperatingSystem.Unknown; + } + } + + /// + /// Checks if this application (including version number) is the only instance currently running. + /// + /// + /// true if this instance is the only instance; otherwise, false. + /// + public static bool IsTheOnlyInstance + { + get + { + lock (SyncLock) + { + try + { + // Try to open existing mutex. + using var _ = Mutex.OpenExisting(ApplicationMutexName); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + try + { + // If exception occurred, there is no such mutex. + using var appMutex = new Mutex(true, ApplicationMutexName); + $"Application Mutex created {appMutex} named '{ApplicationMutexName}'".Debug( + typeof(SwanRuntime)); + + // Only one instance. + return true; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + // Sometimes the user can't create the Global Mutex + } + } + + // More than one instance. + return false; + } + } + } + + /// + /// Gets a value indicating whether this application instance is using the MONO runtime. + /// + /// + /// true if this instance is using MONO runtime; otherwise, false. + /// + public static bool IsUsingMonoRuntime => Type.GetType("Mono.Runtime") != null; + + /// + /// Gets the assembly that started the application. + /// + /// + /// The entry assembly. + /// + public static Assembly EntryAssembly => EntryAssemblyLazy.Value; + + /// + /// Gets the name of the entry assembly. + /// + /// + /// The name of the entry assembly. + /// + public static AssemblyName EntryAssemblyName => EntryAssemblyLazy.Value.GetName(); + + /// + /// Gets the entry assembly version. + /// + public static Version EntryAssemblyVersion => EntryAssemblyName.Version; + + /// + /// Gets the full path to the folder containing the assembly that started the application. + /// + /// + /// The entry assembly directory. + /// + public static string EntryAssemblyDirectory + { + get + { + var uri = new UriBuilder(EntryAssembly.CodeBase); + var path = Uri.UnescapeDataString(uri.Path); + return Path.GetDirectoryName(path); + } + } + + /// + /// Gets the name of the company. + /// + /// + /// The name of the company. + /// + public static string CompanyName => CompanyNameLazy.Value; + + /// + /// Gets the name of the product. + /// + /// + /// The name of the product. + /// + public static string ProductName => ProductNameLazy.Value; + + /// + /// Gets the trademark. + /// + /// + /// The product trademark. + /// + public static string ProductTrademark => ProductTrademarkLazy.Value; + + /// + /// Gets a local storage path with a version. + /// + /// + /// The local storage path. + /// + public static string LocalStoragePath + { + get + { + var localAppDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + EntryAssemblyName.Name); + + var returnPath = Path.Combine(localAppDataPath, EntryAssemblyVersion.ToString()); + + if (!Directory.Exists(returnPath)) + { + Directory.CreateDirectory(returnPath); + } + + return returnPath; + } + } + + #endregion + + #region Methods + + /// + /// Build a full path pointing to the current user's desktop with the given filename. + /// + /// The filename. + /// + /// The fully qualified location of path, such as "C:\MyFile.txt". + /// + /// filename. + public static string GetDesktopFilePath(string filename) + { + if (string.IsNullOrWhiteSpace(filename)) + throw new ArgumentNullException(nameof(filename)); + + var pathWithFilename = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), + filename); + + return Path.GetFullPath(pathWithFilename); + } + + #endregion + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Terminal.Graphics.cs b/Vendor/Swan.Lite-3.1.0/Terminal.Graphics.cs new file mode 100644 index 0000000..1a1b5be --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Terminal.Graphics.cs @@ -0,0 +1,37 @@ +namespace Swan +{ + /// + /// A console terminal helper to create nicer output and receive input from the user + /// This class is thread-safe :). + /// + public static partial class Terminal + { + /// + /// Represents a Table to print in console. + /// + private static class Table + { + public static void Vertical() => Write('\u2502', Settings.BorderColor); + + public static void RightTee() => Write('\u2524', Settings.BorderColor); + + public static void TopRight() => Write('\u2510', Settings.BorderColor); + + public static void BottomLeft() => Write('\u2514', Settings.BorderColor); + + public static void BottomTee() => Write('\u2534', Settings.BorderColor); + + public static void TopTee() => Write('\u252c', Settings.BorderColor); + + public static void LeftTee() => Write('\u251c', Settings.BorderColor); + + public static void Horizontal(int length) => Write(new string('\u2500', length), Settings.BorderColor); + + public static void Tee() => Write('\u253c', Settings.BorderColor); + + public static void BottomRight() => Write('\u2518', Settings.BorderColor); + + public static void TopLeft() => Write('\u250C', Settings.BorderColor); + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Terminal.Interaction.cs b/Vendor/Swan.Lite-3.1.0/Terminal.Interaction.cs new file mode 100644 index 0000000..dbfacb2 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Terminal.Interaction.cs @@ -0,0 +1,260 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Swan.Logging; + +namespace Swan +{ + /// + /// A console terminal helper to create nicer output and receive input from the user + /// This class is thread-safe :). + /// + public static partial class Terminal + { + #region ReadKey + + /// + /// Reads a key from the Terminal. This is the closest equivalent to Console.ReadKey. + /// + /// if set to true the pressed key will not be rendered to the output. + /// if set to true the output will continue to be shown. + /// This is useful for services and daemons that are running as console applications and wait for a key to exit the program. + /// The console key information. + public static ConsoleKeyInfo ReadKey(bool intercept, bool disableLocking = false) + { + if (!IsConsolePresent) return default; + if (disableLocking) return Console.ReadKey(intercept); + + lock (SyncLock) + { + Flush(); + InputDone.Reset(); + + try + { + Console.CursorVisible = true; + return Console.ReadKey(intercept); + } + finally + { + Console.CursorVisible = false; + InputDone.Set(); + } + } + } + + /// + /// Reads a key from the Terminal. + /// + /// The prompt. + /// if set to true [prevent echo]. + /// The console key information. + public static ConsoleKeyInfo ReadKey(string prompt, bool preventEcho = true) + { + if (!IsConsolePresent) return default; + + lock (SyncLock) + { + if (prompt != null) + { + Write($"{GetNowFormatted()}{Settings.UserInputPrefix} << {prompt} ", ConsoleColor.White); + } + + var input = ReadKey(true); + var echo = preventEcho ? string.Empty : input.Key.ToString(); + WriteLine(echo); + return input; + } + } + + #endregion + + #region Other Terminal Read Methods + + /// + /// Clears the screen. + /// + public static void Clear() + { + Flush(); + Console.Clear(); + } + + /// + /// Reads a line of text from the console. + /// + /// The read line. + public static string? ReadLine() + { + if (IsConsolePresent == false) return default; + + lock (SyncLock) + { + Flush(); + InputDone.Reset(); + + try + { + Console.CursorVisible = true; + return Console.ReadLine(); + } + finally + { + Console.CursorVisible = false; + InputDone.Set(); + } + } + } + + /// + /// Reads a line from the input. + /// + /// The prompt. + /// The read line. + public static string? ReadLine(string prompt) + { + if (!IsConsolePresent) return null; + + lock (SyncLock) + { + Write($"{GetNowFormatted()}{Settings.UserInputPrefix} << {prompt}: ", ConsoleColor.White); + + return ReadLine(); + } + } + + /// + /// Reads a number from the input. If unable to parse, it returns the default number. + /// + /// The prompt. + /// The default number. + /// + /// Conversions of string representation of a number to its 32-bit signed integer equivalent. + /// + public static int ReadNumber(string prompt, int defaultNumber) + { + if (!IsConsolePresent) return defaultNumber; + + lock (SyncLock) + { + Write($"{GetNowFormatted()}{Settings.UserInputPrefix} << {prompt} (default is {defaultNumber}): ", + ConsoleColor.White); + + var input = ReadLine(); + return int.TryParse(input, out var parsedInt) ? parsedInt : defaultNumber; + } + } + + /// + /// Creates a table prompt where the user can enter an option based on the options dictionary provided. + /// + /// The title. + /// The options. + /// Any key option. + /// + /// A value that identifies the console key that was pressed. + /// + /// options. + public static ConsoleKeyInfo ReadPrompt( + string title, + IDictionary options, + string anyKeyOption) + { + if (!IsConsolePresent) return default; + + if (options == null) + throw new ArgumentNullException(nameof(options)); + + const ConsoleColor textColor = ConsoleColor.White; + var lineLength = Console.WindowWidth; + var lineAlign = -(lineLength - 2); + var textFormat = "{0," + lineAlign + "}"; + + // lock the output as an atomic operation + lock (SyncLock) + { + { + // Top border + Table.TopLeft(); + Table.Horizontal(-lineAlign); + Table.TopRight(); + } + + { + // Title + Table.Vertical(); + var titleText = string.Format(CultureInfo.CurrentCulture, + textFormat, + string.IsNullOrWhiteSpace(title) ? " Select an option from the list below." : $" {title}"); + Write(titleText, textColor); + Table.Vertical(); + } + + { + // Title Bottom + Table.LeftTee(); + Table.Horizontal(lineLength - 2); + Table.RightTee(); + } + + // Options + foreach (var kvp in options) + { + Table.Vertical(); + Write(string.Format( + CultureInfo.CurrentCulture, + textFormat, + $" {"[ " + kvp.Key + " ]",-10} {kvp.Value}"), + textColor); + Table.Vertical(); + } + + // Any Key Options + if (string.IsNullOrWhiteSpace(anyKeyOption) == false) + { + Table.Vertical(); + Write(string.Format(CultureInfo.CurrentCulture, textFormat, " "), ConsoleColor.Gray); + Table.Vertical(); + + Table.Vertical(); + Write(string.Format( + CultureInfo.CurrentCulture, + textFormat, + $" {" ",-10} {anyKeyOption}"), + ConsoleColor.Gray); + Table.Vertical(); + } + + { + // Input section + Table.LeftTee(); + Table.Horizontal(lineLength - 2); + Table.RightTee(); + + Table.Vertical(); + Write(string.Format(CultureInfo.CurrentCulture, textFormat, Settings.UserOptionText), + ConsoleColor.Green); + Table.Vertical(); + + Table.BottomLeft(); + Table.Horizontal(lineLength - 2); + Table.BottomRight(); + } + } + + var inputLeft = Settings.UserOptionText.Length + 3; + + SetCursorPosition(inputLeft, CursorTop - 1); + var userInput = ReadKey(true); + Write(userInput.Key.ToString(), ConsoleColor.Gray); + + SetCursorPosition(0, CursorTop + 2); + return userInput; + } + + #endregion + + private static string GetNowFormatted() => + $" {(string.IsNullOrWhiteSpace(TextLogger.LoggingTimeFormat) ? string.Empty : DateTime.Now.ToString(TextLogger.LoggingTimeFormat) + " ")}"; + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Terminal.Output.cs b/Vendor/Swan.Lite-3.1.0/Terminal.Output.cs new file mode 100644 index 0000000..d9b8f28 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Terminal.Output.cs @@ -0,0 +1,97 @@ +using System; + +namespace Swan +{ + /// + /// A console terminal helper to create nicer output and receive input from the user + /// This class is thread-safe :). + /// + public static partial class Terminal + { + /// + /// Writes a character a number of times, optionally adding a new line at the end. + /// + /// The character code. + /// The color. + /// The count. + /// if set to true [new line]. + /// The writer flags. + public static void Write(char charCode, ConsoleColor? color = null, int count = 1, bool newLine = false, TerminalWriters writerFlags = TerminalWriters.StandardOutput) + { + lock (SyncLock) + { + var text = new string(charCode, count); + + if (newLine) + { + text += Environment.NewLine; + } + + var buffer = OutputEncoding.GetBytes(text); + var context = new OutputContext + { + OutputColor = color ?? Settings.DefaultColor, + OutputText = OutputEncoding.GetChars(buffer), + OutputWriters = writerFlags, + }; + + EnqueueOutput(context); + } + } + + /// + /// Writes the specified text in the given color. + /// + /// The text. + /// The color. + /// The writer flags. + public static void Write(string? text, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput) + { + if (text == null) return; + + lock (SyncLock) + { + var buffer = OutputEncoding.GetBytes(text); + var context = new OutputContext + { + OutputColor = color ?? Settings.DefaultColor, + OutputText = OutputEncoding.GetChars(buffer), + OutputWriters = writerFlags, + }; + + EnqueueOutput(context); + } + } + + /// + /// Writes a New Line Sequence to the standard output. + /// + /// The writer flags. + public static void WriteLine(TerminalWriters writerFlags = TerminalWriters.StandardOutput) + => Write(Environment.NewLine, Settings.DefaultColor, writerFlags); + + /// + /// Writes a line of text in the current console foreground color + /// to the standard output. + /// + /// The text. + /// The color. + /// The writer flags. + public static void WriteLine(string text, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput) + => Write($"{text ?? string.Empty}{Environment.NewLine}", color, writerFlags); + + /// + /// As opposed to WriteLine methods, it prepends a Carriage Return character to the text + /// so that the console moves the cursor one position up after the text has been written out. + /// + /// The text. + /// The color. + /// The writer flags. + public static void OverwriteLine(string text, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput) + { + Write($"\r{text ?? string.Empty}", color, writerFlags); + Flush(); + CursorLeft = 0; + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Terminal.Settings.cs b/Vendor/Swan.Lite-3.1.0/Terminal.Settings.cs new file mode 100644 index 0000000..20d8118 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Terminal.Settings.cs @@ -0,0 +1,49 @@ +using System; + +namespace Swan +{ + /// + /// A console terminal helper to create nicer output and receive input from the user + /// This class is thread-safe :). + /// + public static partial class Terminal + { + /// + /// Terminal global settings. + /// + public static class Settings + { + /// + /// Gets or sets the default output color. + /// + /// + /// The default color. + /// + public static ConsoleColor DefaultColor { get; set; } = Console.ForegroundColor; + + /// + /// Gets the color of the border. + /// + /// + /// The color of the border. + /// + public static ConsoleColor BorderColor { get; } = ConsoleColor.DarkGreen; + + /// + /// Gets or sets the user input prefix. + /// + /// + /// The user input prefix. + /// + public static string UserInputPrefix { get; set; } = "USR"; + + /// + /// Gets or sets the user option text. + /// + /// + /// The user option text. + /// + public static string UserOptionText { get; set; } = " Option: "; + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Terminal.cs b/Vendor/Swan.Lite-3.1.0/Terminal.cs new file mode 100644 index 0000000..c407e4c --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Terminal.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Concurrent; +using System.Text; +using System.Threading; +using Swan.Threading; + +namespace Swan +{ + /// + /// A console terminal helper to create nicer output and receive input from the user. + /// This class is thread-safe :). + /// + public static partial class Terminal + { + #region Private Declarations + + private const int OutputFlushInterval = 15; + private static readonly ExclusiveTimer DequeueOutputTimer; + private static readonly object SyncLock = new object(); + private static readonly ConcurrentQueue OutputQueue = new ConcurrentQueue(); + + private static readonly ManualResetEventSlim OutputDone = new ManualResetEventSlim(false); + private static readonly ManualResetEventSlim InputDone = new ManualResetEventSlim(true); + + private static bool? _isConsolePresent; + + #endregion + + #region Constructors + + /// + /// Initializes static members of the class. + /// + static Terminal() + { + lock (SyncLock) + { + if (DequeueOutputTimer != null) return; + + if (IsConsolePresent) + { + Console.CursorVisible = false; + } + + // Here we start the output task, fire-and-forget + DequeueOutputTimer = new ExclusiveTimer(DequeueOutputCycle); + DequeueOutputTimer.Resume(OutputFlushInterval); + } + } + + #endregion + + #region Synchronized Cursor Movement + + /// + /// Gets or sets the cursor left position. + /// + /// + /// The cursor left. + /// + public static int CursorLeft + { + get + { + if (IsConsolePresent == false) return -1; + lock (SyncLock) + { + Flush(); + return Console.CursorLeft; + } + } + set + { + if (IsConsolePresent == false) return; + lock (SyncLock) + { + Flush(); + Console.CursorLeft = value; + } + } + } + + /// + /// Gets or sets the cursor top position. + /// + /// + /// The cursor top. + /// + public static int CursorTop + { + get + { + if (IsConsolePresent == false) return -1; + + lock (SyncLock) + { + Flush(); + return Console.CursorTop; + } + } + set + { + if (IsConsolePresent == false) return; + + lock (SyncLock) + { + Flush(); + Console.CursorTop = value; + } + } + } + + #endregion + + #region Properties + + /// + /// Gets a value indicating whether the Console is present. + /// + /// + /// true if this instance is console present; otherwise, false. + /// + public static bool IsConsolePresent + { + get + { + if (_isConsolePresent == null) + { + _isConsolePresent = true; + try + { + var windowHeight = Console.WindowHeight; + _isConsolePresent = windowHeight >= 0; + } + catch + { + _isConsolePresent = false; + } + } + + return _isConsolePresent.Value; + } + } + + /// + /// Gets the available output writers in a bitwise mask. + /// + /// + /// The available writers. + /// + public static TerminalWriters AvailableWriters => + IsConsolePresent + ? TerminalWriters.StandardError | TerminalWriters.StandardOutput + : TerminalWriters.None; + + /// + /// Gets or sets the output encoding for the current console. + /// + /// + /// The output encoding. + /// + public static Encoding OutputEncoding + { + get => Console.OutputEncoding; + set => Console.OutputEncoding = value; + } + + #endregion + + #region Methods + + /// + /// Waits for all of the queued output messages to be written out to the console. + /// Call this method if it is important to display console text before + /// quitting the application such as showing usage or help. + /// Set the timeout to null or TimeSpan.Zero to wait indefinitely. + /// + /// The timeout. Set the amount of time to black before this method exits. + public static void Flush(TimeSpan? timeout = null) + { + if (timeout == null) timeout = TimeSpan.Zero; + var startTime = DateTime.UtcNow; + + while (OutputQueue.Count > 0) + { + // Manually trigger a timer cycle to run immediately + DequeueOutputTimer.Change(0, OutputFlushInterval); + + // Wait for the output to finish + if (OutputDone.Wait(OutputFlushInterval)) + break; + + // infinite timeout + if (timeout.Value == TimeSpan.Zero) + continue; + + // break if we have reached a timeout condition + if (DateTime.UtcNow.Subtract(startTime) >= timeout.Value) + break; + } + } + + /// + /// Sets the cursor position. + /// + /// The left. + /// The top. + public static void SetCursorPosition(int left, int top) + { + if (!IsConsolePresent) return; + + lock (SyncLock) + { + Flush(); + Console.SetCursorPosition(left.Clamp(0, left), top.Clamp(0, top)); + } + } + + /// + /// Moves the output cursor one line up starting at left position 0 + /// Please note that backlining the cursor does not clear the contents of the + /// previous line so you might need to clear it by writing an empty string the + /// length of the console width. + /// + public static void BacklineCursor() => SetCursorPosition(0, CursorTop - 1); + + /// + /// Writes a standard banner to the standard output + /// containing the company name, product name, assembly version and trademark. + /// + /// The color. + public static void WriteWelcomeBanner(ConsoleColor color = ConsoleColor.Gray) + { + WriteLine($"{SwanRuntime.CompanyName} {SwanRuntime.ProductName} [Version {SwanRuntime.EntryAssemblyVersion}]", color); + WriteLine($"{SwanRuntime.ProductTrademark}", color); + } + + /// + /// Enqueues the output to be written to the console + /// This is the only method that should enqueue to the output + /// Please note that if AvailableWriters is None, then no output will be enqueued. + /// + /// The context. + private static void EnqueueOutput(OutputContext context) + { + lock (SyncLock) + { + var availableWriters = AvailableWriters; + + if (availableWriters == TerminalWriters.None || context.OutputWriters == TerminalWriters.None) + { + OutputDone.Set(); + return; + } + + if ((context.OutputWriters & availableWriters) == TerminalWriters.None) + return; + + OutputDone.Reset(); + OutputQueue.Enqueue(context); + } + } + + /// + /// Runs a Terminal I/O cycle in the thread. + /// + private static void DequeueOutputCycle() + { + if (AvailableWriters == TerminalWriters.None) + { + OutputDone.Set(); + return; + } + + InputDone.Wait(); + + if (OutputQueue.Count <= 0) + { + OutputDone.Set(); + return; + } + + OutputDone.Reset(); + + while (OutputQueue.Count > 0) + { + if (!OutputQueue.TryDequeue(out var context)) continue; + + // Process Console output and Skip over stuff we can't display so we don't stress the output too much. + if (!IsConsolePresent) continue; + + Console.ForegroundColor = context.OutputColor; + + // Output to the standard output + if (context.OutputWriters.HasFlag(TerminalWriters.StandardOutput)) + { + Console.Out.Write(context.OutputText); + } + + // output to the standard error + if (context.OutputWriters.HasFlag(TerminalWriters.StandardError)) + { + Console.Error.Write(context.OutputText); + } + + Console.ResetColor(); + Console.ForegroundColor = context.OriginalColor; + } + } + + #endregion + + #region Output Context + + /// + /// Represents an asynchronous output context. + /// + private sealed class OutputContext + { + /// + /// Initializes a new instance of the class. + /// + public OutputContext() + { + OriginalColor = Settings.DefaultColor; + OutputWriters = IsConsolePresent + ? TerminalWriters.StandardOutput + : TerminalWriters.None; + } + + public ConsoleColor OriginalColor { get; } + public ConsoleColor OutputColor { get; set; } + public char[] OutputText { get; set; } + public TerminalWriters OutputWriters { get; set; } + } + + #endregion + } +} diff --git a/Vendor/Swan.Lite-3.1.0/TerminalWriters.Enums.cs b/Vendor/Swan.Lite-3.1.0/TerminalWriters.Enums.cs new file mode 100644 index 0000000..b1d4b9d --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/TerminalWriters.Enums.cs @@ -0,0 +1,31 @@ +using System; + +namespace Swan +{ + /// + /// Defines a set of bitwise standard terminal writers. + /// + [Flags] + public enum TerminalWriters + { + /// + /// Prevents output + /// + None = 0, + + /// + /// Writes to the Console.Out + /// + StandardOutput = 1, + + /// + /// Writes to the Console.Error + /// + StandardError = 2, + + /// + /// Writes to all possible terminal writers + /// + All = StandardOutput | StandardError, + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Threading/AtomicBoolean.cs b/Vendor/Swan.Lite-3.1.0/Threading/AtomicBoolean.cs new file mode 100644 index 0000000..7db2f17 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/AtomicBoolean.cs @@ -0,0 +1,24 @@ +namespace Swan.Threading +{ + /// + /// Fast, atomic boolean combining interlocked to write value and volatile to read values. + /// + public sealed class AtomicBoolean : AtomicTypeBase + { + /// + /// Initializes a new instance of the class. + /// + /// if set to true [initial value]. + public AtomicBoolean(bool initialValue = default) + : base(initialValue ? 1 : 0) + { + // placeholder + } + + /// + protected override bool FromLong(long backingValue) => backingValue != 0; + + /// + protected override long ToLong(bool value) => value ? 1 : 0; + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Threading/AtomicDateTime.cs b/Vendor/Swan.Lite-3.1.0/Threading/AtomicDateTime.cs new file mode 100644 index 0000000..8bec976 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/AtomicDateTime.cs @@ -0,0 +1,26 @@ +using System; + +namespace Swan.Threading +{ + /// + /// Defines an atomic DateTime. + /// + public sealed class AtomicDateTime : AtomicTypeBase + { + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + public AtomicDateTime(DateTime initialValue) + : base(initialValue.Ticks) + { + // placeholder + } + + /// + protected override DateTime FromLong(long backingValue) => new DateTime(backingValue); + + /// + protected override long ToLong(DateTime value) => value.Ticks; + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Threading/AtomicDouble.cs b/Vendor/Swan.Lite-3.1.0/Threading/AtomicDouble.cs new file mode 100644 index 0000000..b611a08 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/AtomicDouble.cs @@ -0,0 +1,28 @@ +using System; + +namespace Swan.Threading +{ + /// + /// Fast, atomic double combining interlocked to write value and volatile to read values. + /// + public sealed class AtomicDouble : AtomicTypeBase + { + /// + /// Initializes a new instance of the class. + /// + /// if set to true [initial value]. + public AtomicDouble(double initialValue = default) + : base(BitConverter.DoubleToInt64Bits(initialValue)) + { + // placeholder + } + + /// + protected override double FromLong(long backingValue) => + BitConverter.Int64BitsToDouble(backingValue); + + /// + protected override long ToLong(double value) => + BitConverter.DoubleToInt64Bits(value); + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Threading/AtomicEnum.cs b/Vendor/Swan.Lite-3.1.0/Threading/AtomicEnum.cs new file mode 100644 index 0000000..b06af12 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/AtomicEnum.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; + +namespace Swan.Threading +{ + /// + /// Defines an atomic generic Enum. + /// + /// The type of enum. + public sealed class AtomicEnum + where T : struct, IConvertible + { + private long _backingValue; + + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + /// T must be an enumerated type. + public AtomicEnum(T initialValue) + { + if (!Enum.IsDefined(typeof(T), initialValue)) + throw new ArgumentException("T must be an enumerated type"); + + Value = initialValue; + } + + /// + /// Gets or sets the value. + /// + public T Value + { + get => (T)Enum.ToObject(typeof(T), BackingValue); + set => BackingValue = Convert.ToInt64(value); + } + + private long BackingValue + { + get => Interlocked.Read(ref _backingValue); + set => Interlocked.Exchange(ref _backingValue, value); + } + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Threading/AtomicInteger.cs b/Vendor/Swan.Lite-3.1.0/Threading/AtomicInteger.cs new file mode 100644 index 0000000..5cad25d --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/AtomicInteger.cs @@ -0,0 +1,28 @@ +using System; + +namespace Swan.Threading +{ + /// + /// Represents an atomically readable or writable integer. + /// + public class AtomicInteger : AtomicTypeBase + { + /// + /// Initializes a new instance of the class. + /// + /// if set to true [initial value]. + public AtomicInteger(int initialValue = default) + : base(Convert.ToInt64(initialValue)) + { + // placeholder + } + + /// + protected override int FromLong(long backingValue) => + Convert.ToInt32(backingValue); + + /// + protected override long ToLong(int value) => + Convert.ToInt64(value); + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Threading/AtomicLong.cs b/Vendor/Swan.Lite-3.1.0/Threading/AtomicLong.cs new file mode 100644 index 0000000..b675187 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/AtomicLong.cs @@ -0,0 +1,24 @@ +namespace Swan.Threading +{ + /// + /// Fast, atomic long combining interlocked to write value and volatile to read values. + /// + public sealed class AtomicLong : AtomicTypeBase + { + /// + /// Initializes a new instance of the class. + /// + /// if set to true [initial value]. + public AtomicLong(long initialValue = default) + : base(initialValue) + { + // placeholder + } + + /// + protected override long FromLong(long backingValue) => backingValue; + + /// + protected override long ToLong(long value) => value; + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Threading/AtomicTimeSpan.cs b/Vendor/Swan.Lite-3.1.0/Threading/AtomicTimeSpan.cs new file mode 100644 index 0000000..ec27877 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/AtomicTimeSpan.cs @@ -0,0 +1,26 @@ +using System; + +namespace Swan.Threading +{ + /// + /// Represents an atomic TimeSpan type. + /// + public sealed class AtomicTimeSpan : AtomicTypeBase + { + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + public AtomicTimeSpan(TimeSpan initialValue) + : base(initialValue.Ticks) + { + // placeholder + } + + /// + protected override TimeSpan FromLong(long backingValue) => TimeSpan.FromTicks(backingValue); + + /// + protected override long ToLong(TimeSpan value) => value.Ticks < 0 ? 0 : value.Ticks; + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Threading/AtomicTypeBase.cs b/Vendor/Swan.Lite-3.1.0/Threading/AtomicTypeBase.cs new file mode 100644 index 0000000..fae6cd3 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/AtomicTypeBase.cs @@ -0,0 +1,243 @@ +using System; +using System.Threading; + +namespace Swan.Threading +{ + /// + /// Provides a generic implementation of an Atomic (interlocked) type + /// + /// Idea taken from Memory model and .NET operations in article: + /// http://igoro.com/archive/volatile-keyword-in-c-memory-model-explained/. + /// + /// The structure type backed by a 64-bit value. + public abstract class AtomicTypeBase : IComparable, IComparable, IComparable>, IEquatable, IEquatable> + where T : struct, IComparable, IComparable, IEquatable + { + private long _backingValue; + + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + protected AtomicTypeBase(long initialValue) + { + BackingValue = initialValue; + } + + /// + /// Gets or sets the value. + /// + public T Value + { + get => FromLong(BackingValue); + set => BackingValue = ToLong(value); + } + + /// + /// Gets or sets the backing value. + /// + protected long BackingValue + { + get => Interlocked.Read(ref _backingValue); + set => Interlocked.Exchange(ref _backingValue, value); + } + + /// + /// Implements the operator ==. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static bool operator ==(AtomicTypeBase a, T b) => a?.Equals(b) == true; + + /// + /// Implements the operator !=. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static bool operator !=(AtomicTypeBase a, T b) => a?.Equals(b) == false; + + /// + /// Implements the operator >. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static bool operator >(AtomicTypeBase a, T b) => a.CompareTo(b) > 0; + + /// + /// Implements the operator <. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static bool operator <(AtomicTypeBase a, T b) => a.CompareTo(b) < 0; + + /// + /// Implements the operator >=. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static bool operator >=(AtomicTypeBase a, T b) => a.CompareTo(b) >= 0; + + /// + /// Implements the operator <=. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static bool operator <=(AtomicTypeBase a, T b) => a.CompareTo(b) <= 0; + + /// + /// Implements the operator ++. + /// + /// The instance. + /// + /// The result of the operator. + /// + public static AtomicTypeBase operator ++(AtomicTypeBase instance) + { + Interlocked.Increment(ref instance._backingValue); + return instance; + } + + /// + /// Implements the operator --. + /// + /// The instance. + /// + /// The result of the operator. + /// + public static AtomicTypeBase operator --(AtomicTypeBase instance) + { + Interlocked.Decrement(ref instance._backingValue); + return instance; + } + + /// + /// Implements the operator -<. + /// + /// The instance. + /// The operand. + /// + /// The result of the operator. + /// + public static AtomicTypeBase operator +(AtomicTypeBase instance, long operand) + { + instance.BackingValue = instance.BackingValue + operand; + return instance; + } + + /// + /// Implements the operator -. + /// + /// The instance. + /// The operand. + /// + /// The result of the operator. + /// + public static AtomicTypeBase operator -(AtomicTypeBase instance, long operand) + { + instance.BackingValue = instance.BackingValue - operand; + return instance; + } + + /// + /// Compares the value to the other instance. + /// + /// The other instance. + /// 0 if equal, 1 if this instance is greater, -1 if this instance is less than. + /// When types are incompatible. + public int CompareTo(object other) + { + switch (other) + { + case null: + return 1; + case AtomicTypeBase atomic: + return BackingValue.CompareTo(atomic.BackingValue); + case T variable: + return Value.CompareTo(variable); + } + + throw new ArgumentException("Incompatible comparison types"); + } + + /// + /// Compares the value to the other instance. + /// + /// The other instance. + /// 0 if equal, 1 if this instance is greater, -1 if this instance is less than. + public int CompareTo(T other) => Value.CompareTo(other); + + /// + /// Compares the value to the other instance. + /// + /// The other instance. + /// 0 if equal, 1 if this instance is greater, -1 if this instance is less than. + public int CompareTo(AtomicTypeBase other) => BackingValue.CompareTo(other?.BackingValue ?? default); + + /// + /// Determines whether the specified , is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public override bool Equals(object other) + { + switch (other) + { + case AtomicTypeBase atomic: + return Equals(atomic); + case T variable: + return Equals(variable); + } + + return false; + } + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override int GetHashCode() => BackingValue.GetHashCode(); + + /// + public bool Equals(AtomicTypeBase other) => + BackingValue == (other?.BackingValue ?? default); + + /// + public bool Equals(T other) => Equals(Value, other); + + /// + /// Converts from a long value to the target type. + /// + /// The backing value. + /// The value converted form a long value. + protected abstract T FromLong(long backingValue); + + /// + /// Converts from the target type to a long value. + /// + /// The value. + /// The value converted to a long value. + protected abstract long ToLong(T value); + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Threading/CancellationTokenOwner.cs b/Vendor/Swan.Lite-3.1.0/Threading/CancellationTokenOwner.cs new file mode 100644 index 0000000..d09ec0c --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/CancellationTokenOwner.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading; + +namespace Swan.Threading +{ + /// + /// Acts as a but with reusable tokens. + /// + public sealed class CancellationTokenOwner : IDisposable + { + private readonly object _syncLock = new object(); + private bool _isDisposed; + private CancellationTokenSource _tokenSource = new CancellationTokenSource(); + + /// + /// Gets the token of the current. + /// + public CancellationToken Token + { + get + { + lock (_syncLock) + { + return _isDisposed + ? CancellationToken.None + : _tokenSource.Token; + } + } + } + + /// + /// Cancels the last referenced token and creates a new token source. + /// + public void Cancel() + { + lock (_syncLock) + { + if (_isDisposed) return; + _tokenSource.Cancel(); + _tokenSource.Dispose(); + _tokenSource = new CancellationTokenSource(); + } + } + + /// + public void Dispose() => Dispose(true); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + private void Dispose(bool disposing) + { + lock (_syncLock) + { + if (_isDisposed) return; + + if (disposing) + { + _tokenSource.Cancel(); + _tokenSource.Dispose(); + } + + _isDisposed = true; + } + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Threading/ExclusiveTimer.cs b/Vendor/Swan.Lite-3.1.0/Threading/ExclusiveTimer.cs new file mode 100644 index 0000000..3b452fb --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/ExclusiveTimer.cs @@ -0,0 +1,232 @@ +using System; +using System.Threading; + +namespace Swan.Threading +{ + /// + /// A threading implementation that executes at most one cycle at a time + /// in a thread. Callback execution is NOT guaranteed to be carried out + /// on the same thread every time the timer fires. + /// + public sealed class ExclusiveTimer : IDisposable + { + private readonly object _syncLock = new object(); + private readonly ManualResetEventSlim _cycleDoneEvent = new ManualResetEventSlim(true); + private readonly Timer _backingTimer; + private readonly TimerCallback _userCallback; + private readonly AtomicBoolean _isDisposing = new AtomicBoolean(); + private readonly AtomicBoolean _isDisposed = new AtomicBoolean(); + private int _period; + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + /// The state. + /// The due time. + /// The period. + public ExclusiveTimer(TimerCallback timerCallback, object? state, int dueTime, int period) + { + _period = period; + _userCallback = timerCallback; + _backingTimer = new Timer(InternalCallback, state ?? this, dueTime, Timeout.Infinite); + } + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + /// The state. + /// The due time. + /// The period. + public ExclusiveTimer(TimerCallback timerCallback, object? state, TimeSpan dueTime, TimeSpan period) + : this(timerCallback, state, Convert.ToInt32(dueTime.TotalMilliseconds), Convert.ToInt32(period.TotalMilliseconds)) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + public ExclusiveTimer(TimerCallback timerCallback) + : this(timerCallback, null, Timeout.Infinite, Timeout.Infinite) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + /// The due time. + /// The period. + public ExclusiveTimer(Action timerCallback, int dueTime, int period) + : this(s => { timerCallback?.Invoke(); }, null, dueTime, period) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + /// The due time. + /// The period. + public ExclusiveTimer(Action timerCallback, TimeSpan dueTime, TimeSpan period) + : this(s => { timerCallback?.Invoke(); }, null, dueTime, period) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + public ExclusiveTimer(Action timerCallback) + : this(timerCallback, Timeout.Infinite, Timeout.Infinite) + { + // placeholder + } + + /// + /// Gets a value indicating whether this instance is disposing. + /// + /// + /// true if this instance is disposing; otherwise, false. + /// + public bool IsDisposing => _isDisposing.Value; + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + public bool IsDisposed => _isDisposed.Value; + + /// + /// Waits until the time is elapsed. + /// + /// The until date. + /// The cancellation token. + public static void WaitUntil(DateTime untilDate, CancellationToken cancellationToken = default) + { + static void Callback(IWaitEvent waitEvent) + { + try + { + waitEvent.Complete(); + waitEvent.Begin(); + } + catch + { + // ignore + } + } + + using var delayLock = WaitEventFactory.Create(true); + using var timer = new ExclusiveTimer(() => Callback(delayLock), 0, 15); + while (!cancellationToken.IsCancellationRequested && DateTime.UtcNow < untilDate) + delayLock.Wait(); + } + + /// + /// Waits the specified wait time. + /// + /// The wait time. + /// The cancellation token. + public static void Wait(TimeSpan waitTime, CancellationToken cancellationToken = default) => + WaitUntil(DateTime.UtcNow.Add(waitTime), cancellationToken); + + /// + /// Changes the start time and the interval between method invocations for the internal timer. + /// + /// The due time. + /// The period. + public void Change(int dueTime, int period) + { + _period = period; + + _backingTimer.Change(dueTime, Timeout.Infinite); + } + + /// + /// Changes the start time and the interval between method invocations for the internal timer. + /// + /// The due time. + /// The period. + public void Change(TimeSpan dueTime, TimeSpan period) + => Change(Convert.ToInt32(dueTime.TotalMilliseconds), Convert.ToInt32(period.TotalMilliseconds)); + + /// + /// Changes the interval between method invocations for the internal timer. + /// + /// The period. + public void Resume(int period) => Change(0, period); + + /// + /// Changes the interval between method invocations for the internal timer. + /// + /// The period. + public void Resume(TimeSpan period) => Change(TimeSpan.Zero, period); + + /// + /// Pauses this instance. + /// + public void Pause() => Change(Timeout.Infinite, Timeout.Infinite); + + /// + public void Dispose() + { + lock (_syncLock) + { + if (_isDisposed == true || _isDisposing == true) + return; + + _isDisposing.Value = true; + } + + try + { + _cycleDoneEvent.Wait(); + _cycleDoneEvent.Dispose(); + Pause(); + _backingTimer.Dispose(); + } + finally + { + _isDisposed.Value = true; + _isDisposing.Value = false; + } + } + + /// + /// Logic that runs every time the timer hits the due time. + /// + /// The state. + private void InternalCallback(object state) + { + lock (_syncLock) + { + if (IsDisposed || IsDisposing) + return; + } + + if (_cycleDoneEvent.IsSet == false) + return; + + _cycleDoneEvent.Reset(); + + try + { + _userCallback(state); + } + finally + { + _cycleDoneEvent?.Set(); + _backingTimer?.Change(_period, Timeout.Infinite); + } + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Threading/ISyncLocker.cs b/Vendor/Swan.Lite-3.1.0/Threading/ISyncLocker.cs new file mode 100644 index 0000000..be75c8d --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/ISyncLocker.cs @@ -0,0 +1,24 @@ +using System; + +namespace Swan.Threading +{ + /// + /// Defines a generic interface for synchronized locking mechanisms. + /// + public interface ISyncLocker : IDisposable + { + /// + /// Acquires a writer lock. + /// The lock is released when the returned locking object is disposed. + /// + /// A disposable locking object. + IDisposable AcquireWriterLock(); + + /// + /// Acquires a reader lock. + /// The lock is released when the returned locking object is disposed. + /// + /// A disposable locking object. + IDisposable AcquireReaderLock(); + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Threading/IWaitEvent.cs b/Vendor/Swan.Lite-3.1.0/Threading/IWaitEvent.cs new file mode 100644 index 0000000..6b949e8 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/IWaitEvent.cs @@ -0,0 +1,57 @@ +using System; + +namespace Swan.Threading +{ + /// + /// Provides a generalized API for ManualResetEvent and ManualResetEventSlim. + /// + /// + public interface IWaitEvent : IDisposable + { + /// + /// Gets a value indicating whether the event is in the completed state. + /// + bool IsCompleted { get; } + + /// + /// Gets a value indicating whether the Begin method has been called. + /// It returns false after the Complete method is called. + /// + bool IsInProgress { get; } + + /// + /// Returns true if the underlying handle is not closed and it is still valid. + /// + bool IsValid { get; } + + /// + /// Gets a value indicating whether this instance is disposed. + /// + bool IsDisposed { get; } + + /// + /// Enters the state in which waiters need to wait. + /// All future waiters will block when they call the Wait method. + /// + void Begin(); + + /// + /// Leaves the state in which waiters need to wait. + /// All current waiters will continue. + /// + void Complete(); + + /// + /// Waits for the event to be completed. + /// + void Wait(); + + /// + /// Waits for the event to be completed. + /// Returns true when there was no timeout. False if the timeout was reached. + /// + /// The maximum amount of time to wait for. + /// true when there was no timeout. false if the timeout was reached. + bool Wait(TimeSpan timeout); + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Threading/IWorker.cs b/Vendor/Swan.Lite-3.1.0/Threading/IWorker.cs new file mode 100644 index 0000000..f70c60c --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/IWorker.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; + +namespace Swan.Threading +{ + /// + /// Defines a standard API to control background application workers. + /// + public interface IWorker + { + /// + /// Gets the current state of the worker. + /// + WorkerState WorkerState { get; } + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + bool IsDisposed { get; } + + /// + /// Gets a value indicating whether this instance is currently being disposed. + /// + /// + /// true if this instance is disposing; otherwise, false. + /// + bool IsDisposing { get; } + + /// + /// Gets or sets the time interval used to execute cycles. + /// + TimeSpan Period { get; set; } + + /// + /// Gets the name identifier of this worker. + /// + string Name { get; } + + /// + /// Starts execution of worker cycles. + /// + /// The awaitable task. + Task StartAsync(); + + /// + /// Pauses execution of worker cycles. + /// + /// The awaitable task. + Task PauseAsync(); + + /// + /// Resumes execution of worker cycles. + /// + /// The awaitable task. + Task ResumeAsync(); + + /// + /// Permanently stops execution of worker cycles. + /// An interrupt is always sent to the worker. If you wish to stop + /// the worker without interrupting then call the + /// method, await it, and finally call the method. + /// + /// The awaitable task. + Task StopAsync(); + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Threading/IWorkerDelayProvider.cs b/Vendor/Swan.Lite-3.1.0/Threading/IWorkerDelayProvider.cs new file mode 100644 index 0000000..8868c14 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/IWorkerDelayProvider.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Swan.Threading +{ + /// + /// An interface for a worker cycle delay provider. + /// + public interface IWorkerDelayProvider + { + /// + /// Suspends execution queues a new cycle for execution. The delay is given in + /// milliseconds. When overridden in a derived class the wait handle will be set + /// whenever an interrupt is received. + /// + /// The remaining delay to wait for in the cycle. + /// Contains a reference to a task with the scheduled period delay. + /// The cancellation token to cancel waiting. + void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token); + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Threading/PeriodicTask.cs b/Vendor/Swan.Lite-3.1.0/Threading/PeriodicTask.cs new file mode 100644 index 0000000..11aaf37 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/PeriodicTask.cs @@ -0,0 +1,101 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Swan.Logging; + +namespace Swan.Threading +{ + /// + /// Schedule an action to be periodically executed on the thread pool. + /// + public sealed class PeriodicTask : IDisposable + { + /// + /// The minimum interval between action invocations. + /// The value of this field is equal to 100 milliseconds. + /// + public static readonly TimeSpan MinInterval = TimeSpan.FromMilliseconds(100); + + private readonly Func _action; + private readonly CancellationTokenSource _cancellationTokenSource; + + private TimeSpan _interval; + + /// + /// Initializes a new instance of the class. + /// + /// The interval between invocations of . + /// The callback to invoke periodically. + /// A that can be used to cancel operations. + public PeriodicTask(TimeSpan interval, Func action, CancellationToken cancellationToken = default) + { + _action = action ?? throw new ArgumentNullException(nameof(action)); + _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _interval = ValidateInterval(interval); + + Task.Run(ActionLoop); + } + + /// + /// Finalizes an instance of the class. + /// + ~PeriodicTask() + { + Dispose(false); + } + + /// + /// Gets or sets the interval between periodic action invocations. + /// Changes to this property take effect after next action invocation. + /// + /// + public TimeSpan Interval + { + get => _interval; + set => _interval = ValidateInterval(value); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + } + } + + private static TimeSpan ValidateInterval(TimeSpan value) + => value < MinInterval ? MinInterval : value; + + private async Task ActionLoop() + { + for (; ; ) + { + try + { + await Task.Delay(Interval, _cancellationTokenSource.Token).ConfigureAwait(false); + await _action(_cancellationTokenSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (_cancellationTokenSource.IsCancellationRequested) + { + break; + } + catch (TaskCanceledException) when (_cancellationTokenSource.IsCancellationRequested) + { + break; + } + catch (Exception ex) + { + ex.Log(nameof(PeriodicTask)); + } + } + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Threading/RunnerBase.cs b/Vendor/Swan.Lite-3.1.0/Threading/RunnerBase.cs new file mode 100644 index 0000000..1e3dba5 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/RunnerBase.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Swan.Configuration; +using Swan.Logging; + +namespace Swan.Threading +{ + /// + /// Represents an background worker abstraction with a life cycle and running at a independent thread. + /// + public abstract class RunnerBase : ConfiguredObject, IDisposable + { + private Thread? _worker; + private CancellationTokenSource? _cancelTokenSource; + private ManualResetEvent? _workFinished; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true [is enabled]. + protected RunnerBase(bool isEnabled) + { + Name = GetType().Name; + IsEnabled = isEnabled; + } + + /// + /// Gets the error messages. + /// + /// + /// The error messages. + /// + public List ErrorMessages { get; } = new List(); + + /// + /// Gets the name. + /// + /// + /// The name. + /// + public string Name { get; } + + /// + /// Gets a value indicating whether this instance is running. + /// + /// + /// true if this instance is running; otherwise, false. + /// + public bool IsRunning { get; private set; } + + /// + /// Gets a value indicating whether this instance is enabled. + /// + /// + /// true if this instance is enabled; otherwise, false. + /// + public bool IsEnabled { get; } + + /// + /// Starts this instance. + /// + public virtual void Start() + { + if (IsEnabled == false) + return; + + "Start Requested".Debug(Name); + _cancelTokenSource = new CancellationTokenSource(); + _workFinished = new ManualResetEvent(false); + + _worker = new Thread(() => + { + _workFinished.Reset(); + IsRunning = true; + try + { + Setup(); + DoBackgroundWork(_cancelTokenSource.Token); + } + catch (ThreadAbortException) + { + $"{nameof(ThreadAbortException)} caught.".Warn(Name); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + $"{ex.GetType()}: {ex.Message}\r\n{ex.StackTrace}".Error(Name); + } + finally + { + Cleanup(); + _workFinished?.Set(); + IsRunning = false; + "Stopped Completely".Debug(Name); + } + }) + { + IsBackground = true, + Name = $"{Name}Thread", + }; + + _worker.Start(); + } + + /// + /// Stops this instance. + /// + public virtual void Stop() + { + if (IsEnabled == false || IsRunning == false) + return; + + "Stop Requested".Debug(Name); + _cancelTokenSource?.Cancel(); + var waitRetries = 5; + while (waitRetries >= 1) + { + if (_workFinished?.WaitOne(250) ?? true) + { + waitRetries = -1; + break; + } + + waitRetries--; + } + + if (waitRetries < 0) + { + "Workbench stopped gracefully".Debug(Name); + } + else + { + "Did not respond to stop request. Aborting thread and waiting . . .".Warn(Name); + _worker?.Abort(); + + if (_workFinished?.WaitOne(5000) == false) + "Waited and no response. Worker might have been left in an inconsistent state.".Error(Name); + else + "Waited for worker and it finally responded (OK).".Debug(Name); + } + + _workFinished?.Dispose(); + _workFinished = null; + } + + /// + public void Dispose() + { + _cancelTokenSource?.Dispose(); + _workFinished?.Dispose(); + } + + /// + /// Setups this instance. + /// + protected void Setup() + { + EnsureConfigurationNotLocked(); + OnSetup(); + LockConfiguration(); + } + + /// + /// Cleanups this instance. + /// + protected virtual void Cleanup() + { + // empty + } + + /// + /// Called when [setup]. + /// + protected virtual void OnSetup() + { + // empty + } + + /// + /// Does the background work. + /// + /// The ct. + protected abstract void DoBackgroundWork(CancellationToken cancellationToken); + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Threading/SyncLockerFactory.cs b/Vendor/Swan.Lite-3.1.0/Threading/SyncLockerFactory.cs new file mode 100644 index 0000000..ab2f470 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/SyncLockerFactory.cs @@ -0,0 +1,185 @@ +using System; +using System.Threading; + +namespace Swan.Threading +{ + /// + /// Provides factory methods to create synchronized reader-writer locks + /// that support a generalized locking and releasing api and syntax. + /// + public static class SyncLockerFactory + { + #region Enums and Interfaces + + /// + /// Enumerates the locking operations. + /// + private enum LockHolderType + { + Read, + Write, + } + + /// + /// Defines methods for releasing locks. + /// + private interface ISyncReleasable + { + /// + /// Releases the writer lock. + /// + void ReleaseWriterLock(); + + /// + /// Releases the reader lock. + /// + void ReleaseReaderLock(); + } + + #endregion + + #region Factory Methods + + /// + /// Creates a reader-writer lock backed by a standard ReaderWriterLock. + /// + /// The synchronized locker. + public static ISyncLocker Create() => new SyncLocker(); + + /// + /// Creates a reader-writer lock backed by a ReaderWriterLockSlim. + /// + /// The synchronized locker. + public static ISyncLocker CreateSlim() => new SyncLockerSlim(); + + /// + /// Creates a reader-writer lock. + /// + /// if set to true it uses the Slim version of a reader-writer lock. + /// The Sync Locker. + public static ISyncLocker Create(bool useSlim) => useSlim ? CreateSlim() : Create(); + + #endregion + + #region Private Classes + + /// + /// The lock releaser. Calling the dispose method releases the lock entered by the parent SyncLocker. + /// + /// + private sealed class SyncLockReleaser : IDisposable + { + private readonly ISyncReleasable _parent; + private readonly LockHolderType _operation; + + private bool _isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The parent. + /// The operation. + public SyncLockReleaser(ISyncReleasable parent, LockHolderType operation) + { + _parent = parent; + _operation = operation; + } + + /// + public void Dispose() + { + if (_isDisposed) return; + _isDisposed = true; + + if (_operation == LockHolderType.Read) + _parent.ReleaseReaderLock(); + else + _parent.ReleaseWriterLock(); + } + } + + /// + /// The Sync Locker backed by a ReaderWriterLock. + /// + /// + /// + private sealed class SyncLocker : ISyncLocker, ISyncReleasable + { + private bool _isDisposed; + private ReaderWriterLock? _locker = new ReaderWriterLock(); + + /// + public IDisposable AcquireReaderLock() + { + _locker?.AcquireReaderLock(Timeout.Infinite); + return new SyncLockReleaser(this, LockHolderType.Read); + } + + /// + public IDisposable AcquireWriterLock() + { + _locker?.AcquireWriterLock(Timeout.Infinite); + return new SyncLockReleaser(this, LockHolderType.Write); + } + + /// + public void ReleaseWriterLock() => _locker?.ReleaseWriterLock(); + + /// + public void ReleaseReaderLock() => _locker?.ReleaseReaderLock(); + + /// + public void Dispose() + { + if (_isDisposed) return; + _isDisposed = true; + _locker?.ReleaseLock(); + _locker = null; + } + } + + /// + /// The Sync Locker backed by ReaderWriterLockSlim. + /// + /// + /// + private sealed class SyncLockerSlim : ISyncLocker, ISyncReleasable + { + private bool _isDisposed; + + private ReaderWriterLockSlim _locker + = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + + /// + public IDisposable AcquireReaderLock() + { + _locker?.EnterReadLock(); + return new SyncLockReleaser(this, LockHolderType.Read); + } + + /// + public IDisposable AcquireWriterLock() + { + _locker?.EnterWriteLock(); + return new SyncLockReleaser(this, LockHolderType.Write); + } + + /// + public void ReleaseWriterLock() => _locker?.ExitWriteLock(); + + /// + public void ReleaseReaderLock() => _locker?.ExitReadLock(); + + /// + public void Dispose() + { + if (_isDisposed) return; + _isDisposed = true; + _locker?.Dispose(); + _locker = null; + } + } + + #endregion + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Threading/WaitEventFactory.cs b/Vendor/Swan.Lite-3.1.0/Threading/WaitEventFactory.cs new file mode 100644 index 0000000..ed9076f --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/WaitEventFactory.cs @@ -0,0 +1,204 @@ +using System; +using System.Threading; + +namespace Swan.Threading +{ + /// + /// Provides a Manual Reset Event factory with a unified API. + /// + /// + /// The following example shows how to use the WaitEventFactory class. + /// + /// using Swan.Threading; + /// + /// public class Example + /// { + /// // create a WaitEvent using the slim version + /// private static readonly IWaitEvent waitEvent = WaitEventFactory.CreateSlim(false); + /// + /// public static void Main() + /// { + /// Task.Factory.StartNew(() => + /// { + /// DoWork(1); + /// }); + /// + /// Task.Factory.StartNew(() => + /// { + /// DoWork(2); + /// }); + /// + /// // send first signal + /// waitEvent.Complete(); + /// waitEvent.Begin(); + /// + /// Thread.Sleep(TimeSpan.FromSeconds(2)); + /// + /// // send second signal + /// waitEvent.Complete(); + /// + /// Terminal.Readline(); + /// } + /// + /// public static void DoWork(int taskNumber) + /// { + /// $"Data retrieved:{taskNumber}".WriteLine(); + /// waitEvent.Wait(); + /// + /// Thread.Sleep(TimeSpan.FromSeconds(2)); + /// $"All finished up {taskNumber}".WriteLine(); + /// } + /// } + /// + /// + public static class WaitEventFactory + { + #region Factory Methods + + /// + /// Creates a Wait Event backed by a standard ManualResetEvent. + /// + /// if initially set to completed. Generally true. + /// The Wait Event. + public static IWaitEvent Create(bool isCompleted) => new WaitEvent(isCompleted); + + /// + /// Creates a Wait Event backed by a ManualResetEventSlim. + /// + /// if initially set to completed. Generally true. + /// The Wait Event. + public static IWaitEvent CreateSlim(bool isCompleted) => new WaitEventSlim(isCompleted); + + /// + /// Creates a Wait Event backed by a ManualResetEventSlim. + /// + /// if initially set to completed. Generally true. + /// if set to true creates a slim version of the wait event. + /// The Wait Event. + public static IWaitEvent Create(bool isCompleted, bool useSlim) => useSlim ? CreateSlim(isCompleted) : Create(isCompleted); + + #endregion + + #region Backing Classes + + /// + /// Defines a WaitEvent backed by a ManualResetEvent. + /// + private class WaitEvent : IWaitEvent + { + private ManualResetEvent? _event; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true [is completed]. + public WaitEvent(bool isCompleted) + { + _event = new ManualResetEvent(isCompleted); + } + + /// + public bool IsDisposed { get; private set; } + + /// + public bool IsValid + { + get + { + if (IsDisposed || _event == null) + return false; + + if (_event?.SafeWaitHandle?.IsClosed ?? true) + return false; + + return !(_event?.SafeWaitHandle?.IsInvalid ?? true); + } + } + + /// + public bool IsCompleted => IsValid == false || (_event?.WaitOne(0) ?? true); + + /// + public bool IsInProgress => !IsCompleted; + + /// + public void Begin() => _event?.Reset(); + + /// + public void Complete() => _event?.Set(); + + /// + void IDisposable.Dispose() + { + if (IsDisposed) return; + IsDisposed = true; + + _event?.Set(); + _event?.Dispose(); + _event = null; + } + + /// + public void Wait() => _event?.WaitOne(); + + /// + public bool Wait(TimeSpan timeout) => _event?.WaitOne(timeout) ?? true; + } + + /// + /// Defines a WaitEvent backed by a ManualResetEventSlim. + /// + private class WaitEventSlim : IWaitEvent + { + private ManualResetEventSlim? _event; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true [is completed]. + public WaitEventSlim(bool isCompleted) + { + _event = new ManualResetEventSlim(isCompleted); + } + + /// + public bool IsDisposed { get; private set; } + + /// + public bool IsValid => + !IsDisposed && _event?.WaitHandle?.SafeWaitHandle != null && + (!_event.WaitHandle.SafeWaitHandle.IsClosed && !_event.WaitHandle.SafeWaitHandle.IsInvalid); + + /// + public bool IsCompleted => IsValid == false || _event?.IsSet == true; + + /// + public bool IsInProgress => !IsCompleted; + + /// + public void Begin() => _event?.Reset(); + + /// + public void Complete() => _event?.Set(); + + /// + void IDisposable.Dispose() + { + if (IsDisposed) return; + IsDisposed = true; + + _event?.Set(); + _event?.Dispose(); + _event = null; + } + + /// + public void Wait() => _event?.Wait(); + + /// + public bool Wait(TimeSpan timeout) => _event?.Wait(timeout) ?? true; + } + + #endregion + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Threading/WorkerState.cs b/Vendor/Swan.Lite-3.1.0/Threading/WorkerState.cs new file mode 100644 index 0000000..6e5feee --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Threading/WorkerState.cs @@ -0,0 +1,33 @@ +namespace Swan.Threading +{ + /// + /// Enumerates the different states in which a worker can be. + /// + public enum WorkerState + { + /// + /// The worker has been created and it is ready to start. + /// + Created, + + /// + /// The worker is running it cycle logic. + /// + Running, + + /// + /// The worker is running its delay logic. + /// + Waiting, + + /// + /// The worker is in the paused or suspended state. + /// + Paused, + + /// + /// The worker is stopped and ready for disposal. + /// + Stopped, + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Validators/IValidator.cs b/Vendor/Swan.Lite-3.1.0/Validators/IValidator.cs new file mode 100644 index 0000000..eb30992 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Validators/IValidator.cs @@ -0,0 +1,21 @@ +namespace Swan.Validators +{ + /// + /// A simple Validator interface. + /// + public interface IValidator + { + /// + /// The error message. + /// + string ErrorMessage { get; } + + /// + /// Checks if a value is valid. + /// + /// The type. + /// The value. + /// True if it is valid.False if it is not. + bool IsValid(T value); + } +} \ No newline at end of file diff --git a/Vendor/Swan.Lite-3.1.0/Validators/ObjectValidationResult.cs b/Vendor/Swan.Lite-3.1.0/Validators/ObjectValidationResult.cs new file mode 100644 index 0000000..9054aa8 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Validators/ObjectValidationResult.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Swan.Validators +{ + /// + /// Defines a validation result containing all validation errors and their properties. + /// + public class ObjectValidationResult + { + private readonly List _errors = new List(); + + /// + /// A list of errors. + /// + public IReadOnlyList Errors => _errors; + + /// + /// true if there are no errors; otherwise, false. + /// + public bool IsValid => !Errors.Any(); + + /// + /// Adds an error with a specified property name. + /// + /// The property name. + /// The error message. + public void Add(string propertyName, string errorMessage) => + _errors.Add(new ValidationError(errorMessage, propertyName)); + + /// + /// Defines a validation error. + /// + public class ValidationError + { + /// + /// Initializes a new instance of the class. + /// + /// Name of the property. + /// The error message. + public ValidationError(string propertyName, string errorMessage) + { + PropertyName = propertyName; + ErrorMessage = errorMessage; + } + + /// + /// The property name. + /// + public string PropertyName { get; } + + /// + /// The message error. + /// + public string ErrorMessage { get; } + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Validators/ObjectValidator.cs b/Vendor/Swan.Lite-3.1.0/Validators/ObjectValidator.cs new file mode 100644 index 0000000..59cef81 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Validators/ObjectValidator.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Swan.Reflection; + +namespace Swan.Validators +{ + /// + /// Represents an object validator. + /// + /// + /// The following code describes how to perform a simple object validation. + /// + /// using Swan.Validators; + /// + /// class Example + /// { + /// public static void Main() + /// { + /// // create an instance of ObjectValidator + /// var obj = new ObjectValidator(); + /// + /// // Add a validation to the 'Simple' class with a custom error message + /// obj.AddValidator<Simple>(x => + /// !string.IsNullOrEmpty(x.Name), "Name must not be empty"); + /// + /// // check if object is valid + /// var res = obj.IsValid(new Simple { Name = "Name" }); + /// } + /// + /// class Simple + /// { + /// public string Name { get; set; } + /// } + /// } + /// + /// + /// The following code shows of to validate an object with a custom validator and some attributes using the Runtime ObjectValidator singleton. + /// + /// using Swan.Validators; + /// + /// class Example + /// { + /// public static void Main() + /// { + /// // create an instance of ObjectValidator + /// Runtime.ObjectValidator + /// .AddValidator<Simple>(x => + /// !x.Name.Equals("Name"), "Name must not be 'Name'"); + /// + /// // validate object + /// var res = Runtime.ObjectValidator + /// .Validate(new Simple{ Name = "name", Number = 5, Email ="email@mail.com"}) + /// } + /// + /// class Simple + /// { + /// [NotNull] + /// public string Name { get; set; } + /// + /// [Range(1, 10)] + /// public int Number { get; set; } + /// + /// [Email] + /// public string Email { get; set; } + /// } + /// } + /// + /// + public class ObjectValidator + { + private static readonly Lazy LazyInstance = new Lazy(() => new ObjectValidator()); + + private readonly ConcurrentDictionary>> _predicates = + new ConcurrentDictionary>>(); + + /// + /// Gets the current. + /// + /// + /// The current. + /// + public static ObjectValidator Current => LazyInstance.Value; + + /// + /// Validates an object given the specified validators and attributes. + /// + /// The type of the object. + /// The object. + /// A validation result. + public ObjectValidationResult Validate(T target) + { + var errorList = new ObjectValidationResult(); + ValidateObject(target, false, errorList.Add); + + return errorList; + } + + /// + /// Validates an object given the specified validators and attributes. + /// + /// The type. + /// The object. + /// + /// true if the specified object is valid; otherwise, false. + /// + /// obj. + public bool IsValid(T target) => ValidateObject(target); + + /// + /// Adds a validator to a specific class. + /// + /// The type of the object. + /// The predicate that will be evaluated. + /// The message. + /// + /// predicate + /// or + /// message. + /// + public void AddValidator(Predicate predicate, string message) + where T : class + { + if (predicate == null) + throw new ArgumentNullException(nameof(predicate)); + + if (string.IsNullOrEmpty(message)) + throw new ArgumentNullException(message); + + if (!_predicates.TryGetValue(typeof(T), out var existing)) + { + existing = new List>(); + _predicates[typeof(T)] = existing; + } + + existing.Add(Tuple.Create((Delegate)predicate, message)); + } + + private bool ValidateObject(T obj, bool returnOnError = true, Action? action = null) + { + if (Equals(obj, null)) + throw new ArgumentNullException(nameof(obj)); + + if (_predicates.ContainsKey(typeof(T))) + { + foreach (var (@delegate, value) in _predicates[typeof(T)]) + { + if ((bool)@delegate.DynamicInvoke(obj)) continue; + + action?.Invoke(value, string.Empty); + if (returnOnError) return false; + } + } + + var properties = AttributeCache.DefaultCache.Value.RetrieveFromType(); + + foreach (var prop in properties) + { + foreach (var attribute in prop.Value) + { + var val = (IValidator)attribute; + + if (val.IsValid(prop.Key.GetValue(obj, null))) continue; + + action?.Invoke(val.ErrorMessage, prop.Key.Name); + if (returnOnError) return false; + } + } + + return true; + } + } +} diff --git a/Vendor/Swan.Lite-3.1.0/Validators/Validators.cs b/Vendor/Swan.Lite-3.1.0/Validators/Validators.cs new file mode 100644 index 0000000..3cdbe67 --- /dev/null +++ b/Vendor/Swan.Lite-3.1.0/Validators/Validators.cs @@ -0,0 +1,132 @@ +using System; +using System.Text.RegularExpressions; + +namespace Swan.Validators +{ + /// + /// Regex validator. + /// + [AttributeUsage(AttributeTargets.Property)] + public class MatchAttribute : Attribute, IValidator + { + /// + /// Initializes a new instance of the class. + /// + /// A regex string. + /// The error message. + /// Expression. + public MatchAttribute(string regex, string? errorMessage = null) + { + Expression = regex ?? throw new ArgumentNullException(nameof(regex)); + ErrorMessage = errorMessage ?? "String does not match the specified regular expression"; + } + + /// + /// The string regex used to find a match. + /// + public string Expression { get; } + + /// + public string ErrorMessage { get; internal set; } + + /// + public bool IsValid(T value) + { + if (Equals(value, default(T))) + return false; + + return !(value is string) + ? throw new ArgumentException("Property is not a string") + : Regex.IsMatch(value.ToString(), Expression); + } + } + + /// + /// Email validator. + /// + [AttributeUsage(AttributeTargets.Property)] + public class EmailAttribute : MatchAttribute + { + private const string EmailRegExp = + @"^(?("")("".+?(? + /// Initializes a new instance of the class. + /// + /// The error message. + public EmailAttribute(string? errorMessage = null) + : base(EmailRegExp, errorMessage ?? "String is not an email") + { + } + } + + /// + /// A not null validator. + /// + [AttributeUsage(AttributeTargets.Property)] + public class NotNullAttribute : Attribute, IValidator + { + /// + public string ErrorMessage => "Value is null"; + + /// + public bool IsValid(T value) => !Equals(default(T), value); + } + + /// + /// A range constraint validator. + /// + [AttributeUsage(AttributeTargets.Property)] + public class RangeAttribute : Attribute, IValidator + { + /// + /// Initializes a new instance of the class. + /// Constructor that takes integer minimum and maximum values. + /// + /// The minimum value. + /// The maximum value. + public RangeAttribute(int min, int max) + { + if (min >= max) + throw new InvalidOperationException("Maximum value must be greater than minimum"); + + Maximum = max; + Minimum = min; + } + + /// + /// Initializes a new instance of the class. + /// Constructor that takes double minimum and maximum values. + /// + /// The minimum value. + /// The maximum value. + public RangeAttribute(double min, double max) + { + if (min >= max) + throw new InvalidOperationException("Maximum value must be greater than minimum"); + + Maximum = max; + Minimum = min; + } + + /// + public string ErrorMessage => "Value is not within the specified range"; + + /// + /// Maximum value for the range. + /// + public IComparable Maximum { get; } + + /// + /// Minimum value for the range. + /// + public IComparable Minimum { get; } + + /// + public bool IsValid(T value) + => value is IComparable comparable + ? comparable.CompareTo(Minimum) >= 0 && comparable.CompareTo(Maximum) <= 0 + : throw new ArgumentException(nameof(value)); + } +}