diff options
Diffstat (limited to 'Net.Http/src')
48 files changed, 11366 insertions, 0 deletions
diff --git a/Net.Http/src/AlternateProtocolBase.cs b/Net.Http/src/AlternateProtocolBase.cs new file mode 100644 index 0000000..929bc33 --- /dev/null +++ b/Net.Http/src/AlternateProtocolBase.cs @@ -0,0 +1,96 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: AlternateProtocolBase.cs +* +* AlternateProtocolBase.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Net.Http.Core; + +namespace VNLib.Net.Http +{ + /// <summary> + /// A base class for all non-http protocol handlers + /// </summary> + public abstract class AlternateProtocolBase : MarshalByRefObject, IAlternateProtocol + { + /// <summary> + /// A cancelation source that allows for canceling running tasks, that is linked + /// to the server that called <see cref="RunAsync(Stream)"/>. + /// </summary> + /// <remarks> + /// This property is only available while the <see cref="RunAsync(Stream)"/> + /// method is executing + /// </remarks> +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + protected CancellationTokenSource CancelSource { get; private set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + + ///<inheritdoc/> + async Task IAlternateProtocol.RunAsync(Stream transport, CancellationToken handlerToken) + { + //Create new cancel source + CancelSource ??= new(); + //Register the token to cancel the source and save the registration for unregister on dispose + CancellationTokenRegistration Registration = handlerToken.Register(CancelSource.Cancel); + try + { + //Call child initialize method + await RunAsync(new AlternateProtocolTransportStreamWrapper(transport)); + CancelSource.Cancel(); + } + finally + { + //dispose the cancelation registration + await Registration.DisposeAsync(); + //Dispose cancel source + CancelSource.Dispose(); + } + } + + /// <summary> + /// Is the current socket connected using transport security + /// </summary> + public virtual bool IsSecure { get; init; } + + /// <summary> + /// Determines if the instance is pending cancelation + /// </summary> + public bool IsCancellationRequested => CancelSource.IsCancellationRequested; + + /// <summary> + /// Cancels all pending operations. This session will be unusable after this function is called + /// </summary> + public virtual void CancelAll() => CancelSource?.Cancel(); + + /// <summary> + /// Called when the protocol swtich handshake has completed and the transport is + /// available for the new protocol + /// </summary> + /// <param name="transport">The transport stream</param> + /// <returns>A task that represents the active use of the transport, and when complete all operations are unwound</returns> + protected abstract Task RunAsync(Stream transport); + } +}
\ No newline at end of file diff --git a/Net.Http/src/ConnectionInfo.cs b/Net.Http/src/ConnectionInfo.cs new file mode 100644 index 0000000..6e1660d --- /dev/null +++ b/Net.Http/src/ConnectionInfo.cs @@ -0,0 +1,166 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: ConnectionInfo.cs +* +* ConnectionInfo.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Net; +using System.Linq; +using System.Text; +using System.Collections.Generic; +using System.Security.Authentication; + +using VNLib.Net.Http.Core; +using VNLib.Utils.Extensions; + +namespace VNLib.Net.Http +{ + ///<inheritdoc/> + internal sealed class ConnectionInfo : IConnectionInfo + { + private HttpContext Context; + + ///<inheritdoc/> + public Uri RequestUri => Context.Request.Location; + ///<inheritdoc/> + public string Path => RequestUri.LocalPath; + ///<inheritdoc/> + public string? UserAgent => Context.Request.UserAgent; + ///<inheritdoc/> + public IHeaderCollection Headers { get; private set; } + ///<inheritdoc/> + public bool CrossOrigin { get; } + ///<inheritdoc/> + public bool IsWebSocketRequest { get; } + ///<inheritdoc/> + public ContentType ContentType => Context.Request.ContentType; + ///<inheritdoc/> + public HttpMethod Method => Context.Request.Method; + ///<inheritdoc/> + public HttpVersion ProtocolVersion => Context.Request.HttpVersion; + ///<inheritdoc/> + public bool IsSecure => Context.Request.EncryptionVersion != SslProtocols.None; + ///<inheritdoc/> + public SslProtocols SecurityProtocol => Context.Request.EncryptionVersion; + ///<inheritdoc/> + public Uri? Origin => Context.Request.Origin; + ///<inheritdoc/> + public Uri? Referer => Context.Request.Referrer; + ///<inheritdoc/> + public Tuple<long, long>? Range => Context.Request.Range; + ///<inheritdoc/> + public IPEndPoint LocalEndpoint => Context.Request.LocalEndPoint; + ///<inheritdoc/> + public IPEndPoint RemoteEndpoint => Context.Request.RemoteEndPoint; + ///<inheritdoc/> + public Encoding Encoding => Context.ParentServer.Config.HttpEncoding; + ///<inheritdoc/> + public IReadOnlyDictionary<string, string> RequestCookies => Context.Request.Cookies; + ///<inheritdoc/> + public IEnumerable<string> Accept => Context.Request.Accept; + ///<inheritdoc/> + public TransportSecurityInfo? TransportSecurity => Context.GetSecurityInfo(); + + ///<inheritdoc/> + public bool Accepts(ContentType type) + { + //Get the content type string from he specified content type + string contentType = HttpHelpers.GetContentTypeString(type); + return Accepts(contentType); + } + ///<inheritdoc/> + public bool Accepts(string contentType) + { + if (AcceptsAny()) + { + return true; + } + + //If client accepts exact requested encoding + if (Accept.Contains(contentType)) + { + return true; + } + + //Search accept types to determine if the content type is acceptable + bool accepted = Accept + .Where(ctype => + { + //Get prinary side of mime type + ReadOnlySpan<char> primary = contentType.AsSpan().SliceBeforeParam('/'); + ReadOnlySpan<char> ctSubType = ctype.AsSpan().SliceBeforeParam('/'); + //See if accepts any subtype, or the primary sub-type matches + return ctSubType[0] == '*' || ctSubType.Equals(primary, StringComparison.OrdinalIgnoreCase); + }).Any(); + return accepted; + } + /// <summary> + /// Determines if the connection accepts any content type + /// </summary> + /// <returns>true if the connection accepts any content typ, false otherwise</returns> + private bool AcceptsAny() + { + //Accept any if no accept header was present, or accept all value */* + return Context.Request.Accept.Count == 0 || Accept.Where(static t => t.StartsWith("*/*", StringComparison.OrdinalIgnoreCase)).Any(); + } + ///<inheritdoc/> + public void SetCookie(string name, string value, string? domain, string? path, TimeSpan Expires, CookieSameSite sameSite, bool httpOnly, bool secure) + { + //Create the new cookie + HttpCookie cookie = new(name) + { + Value = value, + Domain = domain, + Path = path, + MaxAge = Expires, + //Set the session lifetime flag if the timeout is max value + IsSession = Expires == TimeSpan.MaxValue, + //If the connection is cross origin, then we need to modify the secure and samsite values + SameSite = CrossOrigin ? CookieSameSite.None : sameSite, + Secure = secure | CrossOrigin, + HttpOnly = httpOnly + }; + //Set the cookie + Context.Response.AddCookie(cookie); + } + + internal ConnectionInfo(HttpContext ctx) + { + //Create new header collection + Headers = new VnHeaderCollection(ctx); + //set co value + CrossOrigin = ctx.Request.IsCrossOrigin(); + //Set websocket status + IsWebSocketRequest = ctx.Request.IsWebSocketRequest(); + //Update the context referrence + Context = ctx; + } + +#nullable disable + internal void Clear() + { + Context = null; + (Headers as VnHeaderCollection).Clear(); + Headers = null; + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/HttpContext.cs b/Net.Http/src/Core/HttpContext.cs new file mode 100644 index 0000000..43d1975 --- /dev/null +++ b/Net.Http/src/Core/HttpContext.cs @@ -0,0 +1,170 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpContext.cs +* +* HttpContext.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Runtime.CompilerServices; + +using VNLib.Utils; +using VNLib.Utils.Memory.Caching; + + +namespace VNLib.Net.Http.Core +{ + internal sealed partial class HttpContext : IConnectionContext, IReusable + { + /// <summary> + /// When set as a response flag, disables response compression for + /// the current request/response flow + /// </summary> + public const ulong COMPRESSION_DISABLED_MSK = 0x01UL; + + /// <summary> + /// The reusable http request container + /// </summary> + public readonly HttpRequest Request; + /// <summary> + /// The reusable response controler + /// </summary> + public readonly HttpResponse Response; + /// <summary> + /// The http server that this context is bound to + /// </summary> + public readonly HttpServer ParentServer; + /// <summary> + /// The shared transport header reader buffer + /// </summary> + public readonly SharedHeaderReaderBuffer RequestBuffer; + + /// <summary> + /// The response entity body container + /// </summary> + public readonly IHttpResponseBody ResponseBody; + + /// <summary> + /// A collection of flags that can be used to control the way the context + /// responds to client requests + /// </summary> + public readonly BitField ContextFlags; + + /// <summary> + /// Gets or sets the alternate application protocol to swtich to + /// </summary> + public IAlternateProtocol? AlternateProtocol { get; set; } + + private readonly ResponseWriter responseWriter; + private ITransportContext? _ctx; + + public HttpContext(HttpServer server) + { + /* + * Local method for retreiving the transport stream, + * this adds protection/debug from response/request + * containers not allowed to maintain referrences + * to a transport stream after it has been released + */ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + Stream GetStream() => _ctx!.ConnectionStream; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + HttpVersion GetVersion() => Request.HttpVersion; + + ParentServer = server; + + //Create new request + Request = new HttpRequest(GetStream); + + //create a new response object + Response = new HttpResponse( + server.Config.HttpEncoding, + ParentServer.Config.ResponseHeaderBufferSize, + ParentServer.Config.ChunkedResponseAccumulatorSize, + GetStream, + GetVersion); + + //The shared request parsing buffer + RequestBuffer = new(server.Config.HeaderBufferSize); + + //Init response writer + ResponseBody = responseWriter = new ResponseWriter(); + + ContextFlags = new(0); + } + + public TransportSecurityInfo? GetSecurityInfo() => _ctx?.GetSecurityInfo(); + + + #region LifeCycle Hooks + + ///<inheritdoc/> + public void InitializeContext(ITransportContext ctx) => _ctx = ctx; + + ///<inheritdoc/> + public void BeginRequest() + { + //Clear all flags + ContextFlags.ClearAll(); + + //Lifecycle on new request + Request.OnNewRequest(); + Response.OnNewRequest(); + RequestBuffer.OnNewRequest(); + + //Initialize the request + Request.Initialize(_ctx!, ParentServer.Config.DefaultHttpVersion); + } + + ///<inheritdoc/> + public void EndRequest() + { + AlternateProtocol = null; + + Request.OnComplete(); + Response.OnComplete(); + RequestBuffer.OnComplete(); + responseWriter.OnComplete(); + } + + void IReusable.Prepare() + { + Request.OnPrepare(); + Response.OnPrepare(); + RequestBuffer.OnPrepare(); + } + + bool IReusable.Release() + { + _ctx = null; + + //Release response/requqests + Request.OnRelease(); + Response.OnRelease(); + RequestBuffer.OnRelease(); + + return true; + } + + #endregion + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/HttpCookie.cs b/Net.Http/src/Core/HttpCookie.cs new file mode 100644 index 0000000..f5408b2 --- /dev/null +++ b/Net.Http/src/Core/HttpCookie.cs @@ -0,0 +1,125 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpCookie.cs +* +* HttpCookie.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +using VNLib.Utils; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; + +namespace VNLib.Net.Http.Core +{ + internal class HttpCookie : IStringSerializeable, IEquatable<HttpCookie> + { + public string Name { get; } + public string? Value { get; init; } + public string? Domain { get; init; } + public string? Path { get; init; } + public TimeSpan MaxAge { get; init; } + public CookieSameSite SameSite { get; init; } + public bool Secure { get; init; } + public bool HttpOnly { get; init; } + public bool IsSession { get; init; } + + public HttpCookie(string name) + { + this.Name = name; + } + + public string Compile() + { + throw new NotImplementedException(); + } + public void Compile(ref ForwardOnlyWriter<char> writer) + { + //set the name of the cookie + writer.Append(Name); + writer.Append('='); + //set name + writer.Append(Value); + //Only set the max age parameter if the cookie is not a session cookie + if (!IsSession) + { + writer.Append("; Max-Age="); + writer.Append((int)MaxAge.TotalSeconds); + } + //Make sure domain is set + if (!string.IsNullOrWhiteSpace(Domain)) + { + writer.Append("; Domain="); + writer.Append(Domain); + } + //Check and set path + if (!string.IsNullOrWhiteSpace(Path)) + { + //Set path + writer.Append("; Path="); + writer.Append(Path); + } + writer.Append("; SameSite="); + //Set the samesite flag based on the enum value + switch (SameSite) + { + case CookieSameSite.None: + writer.Append("None"); + break; + case CookieSameSite.SameSite: + writer.Append("Strict"); + break; + case CookieSameSite.Lax: + default: + writer.Append("Lax"); + break; + } + //Set httponly flag + if (HttpOnly) + { + writer.Append("; HttpOnly"); + } + //Set secure flag + if (Secure) + { + writer.Append("; Secure"); + } + } + public ERRNO Compile(in Span<char> buffer) + { + ForwardOnlyWriter<char> writer = new(buffer); + Compile(ref writer); + return writer.Written; + } + + public override int GetHashCode() => Name.GetHashCode(); + + public override bool Equals(object? obj) + { + return obj is HttpCookie other && Equals(other); + } + + public bool Equals(HttpCookie? other) + { + return other != null && Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase); + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/HttpEvent.cs b/Net.Http/src/Core/HttpEvent.cs new file mode 100644 index 0000000..7d7c1e7 --- /dev/null +++ b/Net.Http/src/Core/HttpEvent.cs @@ -0,0 +1,141 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpEvent.cs +* +* HttpEvent.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Net; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +using VNLib.Net.Http.Core; + +namespace VNLib.Net.Http +{ + internal sealed class HttpEvent : MarshalByRefObject, IHttpEvent + { + private HttpContext Context; + private ConnectionInfo _ci; + + internal HttpEvent(HttpContext ctx) + { + Context = ctx; + _ci = new ConnectionInfo(ctx); + } + + ///<inheritdoc/> + IConnectionInfo IHttpEvent.Server => _ci; + + ///<inheritdoc/> + HttpServer IHttpEvent.OriginServer => Context.ParentServer; + + ///<inheritdoc/> + IReadOnlyDictionary<string, string> IHttpEvent.QueryArgs => Context.Request.RequestBody.QueryArgs; + ///<inheritdoc/> + IReadOnlyDictionary<string, string> IHttpEvent.RequestArgs => Context.Request.RequestBody.RequestArgs; + ///<inheritdoc/> + IReadOnlyList<FileUpload> IHttpEvent.Files => Context.Request.RequestBody.Uploads; + + ///<inheritdoc/> + void IHttpEvent.DisableCompression() => Context.ContextFlags.Set(HttpContext.COMPRESSION_DISABLED_MSK); + + ///<inheritdoc/> + void IHttpEvent.DangerousChangeProtocol(IAlternateProtocol protocolHandler) + { + if(Context.AlternateProtocol != null) + { + throw new InvalidOperationException("A protocol handler was already specified"); + } + + _ = protocolHandler ?? throw new ArgumentNullException(nameof(protocolHandler)); + + //Set 101 status code + Context.Respond(HttpStatusCode.SwitchingProtocols); + Context.AlternateProtocol = protocolHandler; + } + + ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void IHttpEvent.CloseResponse(HttpStatusCode code) => Context.Respond(code); + + ///<inheritdoc/> + void IHttpEvent.CloseResponse(HttpStatusCode code, ContentType type, Stream stream) + { + //Check if the stream is valid. We will need to read the stream, and we will also need to get the length property + if (!stream.CanSeek || !stream.CanRead) + { + throw new IOException("The stream.Length property must be available and the stream must be readable"); + } + + //If stream is empty, ignore it, the server will default to 0 content length and avoid overhead + if (stream.Length == 0) + { + return; + } + + //Set status code + Context.Response.SetStatusCode(code); + + //Finally store the stream input + if(!(Context.ResponseBody as ResponseWriter)!.TrySetResponseBody(stream)) + { + throw new InvalidOperationException("A response body has already been set"); + } + + //Set content type header after body + Context.Response.Headers[HttpResponseHeader.ContentType] = HttpHelpers.GetContentTypeString(type); + } + + ///<inheritdoc/> + void IHttpEvent.CloseResponse(HttpStatusCode code, ContentType type, IMemoryResponseReader entity) + { + //If stream is empty, ignore it, the server will default to 0 content length and avoid overhead + if (entity.Remaining == 0) + { + return; + } + + //Set status code + Context.Response.SetStatusCode(code); + + //Finally store the stream input + if (!(Context.ResponseBody as ResponseWriter)!.TrySetResponseBody(entity)) + { + throw new InvalidOperationException("A response body has already been set"); + } + + //Set content type header after body + Context.Response.Headers[HttpResponseHeader.ContentType] = HttpHelpers.GetContentTypeString(type); + } + +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + internal void Clear() + { + //Clean up referrence types and cleanable objects + Context = null; + _ci.Clear(); + _ci = null; + } +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/HttpServerBase.cs b/Net.Http/src/Core/HttpServerBase.cs new file mode 100644 index 0000000..3a50672 --- /dev/null +++ b/Net.Http/src/Core/HttpServerBase.cs @@ -0,0 +1,312 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpServerBase.cs +* +* HttpServerBase.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +/* + * This file is the base of the HTTP server class that provides + * consts, statics, fields, and properties of the HttpServer class. + * + * Processing of HTTP connections and entities is contained in the + * processing partial file. + * + * Processing is configured to be asynchronous, utilizing .NETs + * asynchronous compilation services. To facilitate this but continue + * to use object caching, reusable stores must be usable across threads + * to function safely with async programming practices. + */ + +using System; +using System.Linq; +using System.Threading; +using System.Net.Sockets; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Security.Authentication; + +using VNLib.Utils.Logging; +using VNLib.Utils.Memory.Caching; + +using VNLib.Net.Http.Core; + +namespace VNLib.Net.Http +{ + + /// <summary> + /// Provides a resource efficient, high performance, single library HTTP(s) server, + /// with extensable processors and transport providers. + /// This class cannot be inherited + /// </summary> + public sealed partial class HttpServer : ICacheHolder + { + /// <summary> + /// The host key that determines a "wildcard" host, meaning the + /// default connection handler when an incomming connection has + /// not specific route + /// </summary> + public const string WILDCARD_KEY = "*"; + + private readonly ITransportProvider Transport; + private readonly IReadOnlyDictionary<string, IWebRoot> ServerRoots; + + #region caches + /// <summary> + /// The cached HTTP1/1 keepalive timeout header value + /// </summary> + private readonly string KeepAliveTimeoutHeaderValue; + /// <summary> + /// Reusable store for obtaining <see cref="HttpContext"/> + /// </summary> + private readonly ObjectRental<HttpContext> ContextStore; + /// <summary> + /// The cached header-line termination value + /// </summary> + private readonly ReadOnlyMemory<byte> HeaderLineTermination; + #endregion + + /// <summary> + /// The <see cref="HttpConfig"/> for the current server + /// </summary> + public HttpConfig Config { get; } + + /// <summary> + /// Gets a value indicating whether the server is listening for connections + /// </summary> + public bool Running { get; private set; } + + private CancellationTokenSource? StopToken; + + /// <summary> + /// Creates a new <see cref="HttpServer"/> with the specified configration copy (using struct). + /// Immutable data structures are initialzed. + /// </summary> + /// <param name="config">The configuration used to create the instance</param> + /// <param name="transport">The transport provider to listen to connections from</param> + /// <param name="sites">A collection of <see cref="IWebRoot"/>s that route incomming connetctions</param> + /// <exception cref="ArgumentException"></exception> + public HttpServer(HttpConfig config, ITransportProvider transport, IEnumerable<IWebRoot> sites) + { + //Validate the configuration + ValidateConfig(in config); + + Config = config; + //Configure roots and their directories + ServerRoots = sites.ToDictionary(static r => r.Hostname, static tv => tv, StringComparer.OrdinalIgnoreCase); + //Compile and store the timeout keepalive header + KeepAliveTimeoutHeaderValue = $"timeout={(int)Config.ConnectionKeepAlive.TotalSeconds}"; + //Store termination for the current instance + HeaderLineTermination = config.HttpEncoding.GetBytes(HttpHelpers.CRLF); + //Create a new context store + ContextStore = ObjectRental.CreateReusable(() => new HttpContext(this)); + //Setup config copy with the internal http pool + Transport = transport; + } + + private static void ValidateConfig(in HttpConfig conf) + { + _ = conf.HttpEncoding ?? throw new ArgumentException("HttpEncoding cannot be null", nameof(conf)); + _ = conf.ServerLog ?? throw new ArgumentException("ServerLog cannot be null", nameof(conf)); + + if (conf.ActiveConnectionRecvTimeout < -1) + { + throw new ArgumentException("ActiveConnectionRecvTimeout cannot be less than -1", nameof(conf)); + } + + //Chunked data accumulator must be at least 64 bytes (arbinrary value) + if (conf.ChunkedResponseAccumulatorSize < 64 || conf.ChunkedResponseAccumulatorSize == int.MaxValue) + { + throw new ArgumentException("ChunkedResponseAccumulatorSize cannot be less than 64 bytes", nameof(conf)); + } + + if (conf.CompressionLimit < 0) + { + throw new ArgumentException("CompressionLimit cannot be less than 0, set to 0 to disable response compression", nameof(conf)); + } + + if (conf.ConnectionKeepAlive < TimeSpan.Zero) + { + throw new ArgumentException("ConnectionKeepAlive cannot be less than 0", nameof(conf)); + } + + if (conf.DefaultHttpVersion == HttpVersion.NotSupported) + { + throw new ArgumentException("DefaultHttpVersion cannot be NotSupported", nameof(conf)); + } + + if (conf.DiscardBufferSize < 64) + { + throw new ArgumentException("DiscardBufferSize cannot be less than 64 bytes", nameof(conf)); + } + + if (conf.FormDataBufferSize < 64) + { + throw new ArgumentException("FormDataBufferSize cannot be less than 64 bytes", nameof(conf)); + } + + if (conf.HeaderBufferSize < 128) + { + throw new ArgumentException("HeaderBufferSize cannot be less than 128 bytes", nameof(conf)); + } + + if (conf.MaxFormDataUploadSize < 0) + { + throw new ArgumentException("MaxFormDataUploadSize cannot be less than 0, set to 0 to disable form-data uploads", nameof(conf)); + } + + if (conf.MaxOpenConnections < 0) + { + throw new ArgumentException("MaxOpenConnections cannot be less than 0", nameof(conf)); + } + + if (conf.MaxRequestHeaderCount < 1) + { + throw new ArgumentException("MaxRequestHeaderCount cannot be less than 1", nameof(conf)); + } + + if (conf.MaxUploadSize < 0) + { + throw new ArgumentException("MaxUploadSize cannot be less than 0", nameof(conf)); + } + + if (conf.ResponseBufferSize < 64) + { + throw new ArgumentException("ResponseBufferSize cannot be less than 64 bytes", nameof(conf)); + } + + if (conf.ResponseHeaderBufferSize < 128) + { + throw new ArgumentException("ResponseHeaderBufferSize cannot be less than 128 bytes", nameof(conf)); + } + + if (conf.SendTimeout < 1) + { + throw new ArgumentException("SendTimeout cannot be less than 1 millisecond", nameof(conf)); + } + } + + /// <summary> + /// Begins listening for connections on configured interfaces for configured hostnames. + /// </summary> + /// <param name="token">A token used to stop listening for incomming connections and close all open websockets</param> + /// <returns>A task that resolves when the server has exited</returns> + /// <exception cref="SocketException"></exception> + /// <exception cref="ThreadStateException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="InvalidOperationException"></exception> + public Task Start(CancellationToken token) + { + StopToken = CancellationTokenSource.CreateLinkedTokenSource(token); + //Start servers with the new token source + Transport.Start(token); + //Start the listen task + return Task.Run(ListenWorkerDoWork, token); + } + + /* + * An SslStream may throw a win32 exception with HRESULT 0x80090327 + * when processing a client certificate (I believe anyway) only + * an issue on some clients (browsers) + */ + + private const int UKNOWN_CERT_AUTH_HRESULT = unchecked((int)0x80090327); + + /// <summary> + /// An invlaid frame size may happen if data is recieved on an open socket + /// but does not contain valid SSL handshake data + /// </summary> + private const int INVALID_FRAME_HRESULT = unchecked((int)0x80131620); + + /* + * A worker task that listens for connections from the transport + */ + private async Task ListenWorkerDoWork() + { + //Set running flag + Running = true; + + Config.ServerLog.Information("HTTP server {hc} listening for connections", GetHashCode()); + //Listen for connections until canceled + while (true) + { + try + { + //Listen for new connection + ITransportContext ctx = await Transport.AcceptAsync(StopToken!.Token); + //Try to dispatch the recieved event + _ = DataReceivedAsync(ctx).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + //Closing, exit loop + break; + } + catch (AuthenticationException ae) + { + Config.ServerLog.Error(ae); + } + catch (Exception ex) + { + Config.ServerLog.Error(ex); + } + } + //Clear all caches + CacheHardClear(); + //Clear running flag + Running = false; + Config.ServerLog.Information("HTTP server {hc} exiting", GetHashCode()); + } + + + ///<inheritdoc/> + ///<exception cref="ObjectDisposedException"></exception> + public void CacheClear() => ContextStore.CacheClear(); + + /// <inheritdoc/> + /// <exception cref="ObjectDisposedException"></exception> + public void CacheHardClear() => ContextStore.CacheHardClear(); + + /// <summary> + /// Writes the specialized log for a socket exception + /// </summary> + /// <param name="se">The socket exception to log</param> + public void WriteSocketExecption(SocketException se) + { + //When clause guards nulls + switch (se.SocketErrorCode) + + { + //Ignore aborted messages + case SocketError.ConnectionAborted: + return; + case SocketError.ConnectionReset: + Config.ServerLog.Debug("Connecion reset by client"); + return; + case SocketError.TimedOut: + Config.ServerLog.Debug("Socket operation timed out"); + return; + default: + Config.ServerLog.Information(se); + break; + } + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/HttpServerProcessing.cs b/Net.Http/src/Core/HttpServerProcessing.cs new file mode 100644 index 0000000..881b66c --- /dev/null +++ b/Net.Http/src/Core/HttpServerProcessing.cs @@ -0,0 +1,387 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpServerProcessing.cs +* +* HttpServerProcessing.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Net.Sockets; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +using VNLib.Utils; +using VNLib.Utils.Logging; +using VNLib.Net.Http.Core; + +namespace VNLib.Net.Http +{ + public sealed partial class HttpServer + { + + private int OpenConnectionCount; + + //Event handler method for processing incoming data events + private async Task DataReceivedAsync(ITransportContext transportContext) + { + //Increment open connection count + Interlocked.Increment(ref OpenConnectionCount); + + //Rent a new context object to reuse + HttpContext context = ContextStore.Rent(); + + try + { + //Set write timeout + transportContext.ConnectionStream.WriteTimeout = Config.SendTimeout; + + //Init stream + context.InitializeContext(transportContext); + + //Keep the transport open and listen for messages as long as keepalive is enabled + do + { + //Set rx timeout low for initial reading + transportContext.ConnectionStream.ReadTimeout = Config.ActiveConnectionRecvTimeout; + + //Process the request + ERRNO keepalive = await ProcessHttpEventAsync(transportContext, context); + + //If the connection is closed, we can return + if (!keepalive) + { + break; + } + + //Set inactive keeaplive timeout + transportContext.ConnectionStream.ReadTimeout = (int)Config.ConnectionKeepAlive.TotalMilliseconds; + + //"Peek" or wait for more data to begin another request (may throw timeout exception when timmed out) + await transportContext.ConnectionStream.ReadAsync(Memory<byte>.Empty, StopToken!.Token); + + } while (true); + } + //Catch wrapped socket exceptions + catch(IOException ioe) when(ioe.InnerException is SocketException se) + { + WriteSocketExecption(se); + } + catch(SocketException se) + { + WriteSocketExecption(se); + } + catch (OperationCanceledException oce) + { + Config.ServerLog.Debug("Failed to receive transport data within a timeout period {m}, connection closed", oce.Message); + } + catch(Exception ex) + { + Config.ServerLog.Error(ex); + } + + //Dec open connection count + Interlocked.Decrement(ref OpenConnectionCount); + + //Return context to store + ContextStore.Return(context); + + //Close the transport async + try + { + await transportContext.CloseConnectionAsync(); + } + catch(Exception ex) + { + Config.ServerLog.Error(ex); + } + } + + + /// <summary> + /// Main event handler for all incoming connections + /// </summary> + /// <param name="transportContext">The <see cref="ITransportContext"/> describing the incoming connection</param> + /// <param name="context">Reusable context object</param> + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + private async Task<ERRNO> ProcessHttpEventAsync(ITransportContext transportContext, HttpContext context) + { + //Prepare http context to process a new message + context.BeginRequest(); + + try + { + //Try to parse the http request (may throw exceptions, let them propagate to the transport layer) + int status = (int)ParseRequest(transportContext, context); + + //Check status code for socket error, if so, return false to close the connection + if (status >= 1000) + { + return false; + } +#if DEBUG + //Write debug request log + if (Config.RequestDebugLog != null) + { + Config.RequestDebugLog.Verbose(context.Request.ToString()); + } +#endif + //process the request + ERRNO keepalive = await ProcessRequestAsync(context, (HttpStatusCode)status); + //Store alternate protocol if set + IAlternateProtocol? alternateProtocol = context.AlternateProtocol; + //Close the response + await context.WriteResponseAsync(StopToken!.Token); + //See if an alterate protocol was specified + if (alternateProtocol != null) + { + //Disable transport timeouts + transportContext.ConnectionStream.WriteTimeout = Timeout.Infinite; + transportContext.ConnectionStream.ReadTimeout = Timeout.Infinite; + //Exec the protocol handler and pass the transport stream + await alternateProtocol.RunAsync(transportContext.ConnectionStream, StopToken!.Token); + + //Clear keepalive flag to close the connection + keepalive = false; + } + + return keepalive; + } + finally + { + //Clean end request + context.EndRequest(); + } + } + + /// <summary> + /// Reads data synchronously from the transport and attempts to parse an HTTP message and + /// built a request. + /// </summary> + /// <param name="transport"></param> + /// <param name="ctx"></param> + /// <returns>0 if the request was successfully parsed, the <see cref="HttpStatusCode"/> + /// to return to the client because the entity could not be processed</returns> + /// <remarks> + /// <para> + /// This method is synchronous for multiple memory optimization reasons, + /// and performance is not expected to be reduced as the transport layer should + /// <br></br> + /// only raise an event when a socket has data available to be read, and entity + /// header sections are expected to fit within a single TCP buffer. + /// </para> + /// </remarks> + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + private HttpStatusCode ParseRequest(ITransportContext transport, HttpContext ctx) + { + //Init parser + TransportReader reader = new (transport.ConnectionStream, ctx.RequestBuffer, Config.HttpEncoding, HeaderLineTermination); + + try + { + Span<char> lineBuf = ctx.RequestBuffer.CharBuffer; + + Http11ParseExtensions.Http1ParseState parseState = new(); + + //Parse the request line + HttpStatusCode code = ctx.Request.Http1ParseRequestLine(ref parseState, ref reader, in lineBuf); + + if (code > 0) + { + return code; + } + //Parse the headers + code = ctx.Request.Http1ParseHeaders(ref parseState, ref reader, Config, in lineBuf); + if (code > 0) + { + return code; + } + //Prepare entity body for request + code = ctx.Request.Http1PrepareEntityBody(ref parseState, ref reader, Config); + if (code > 0) + { + return code; + } + //Success! + return 0; + } + //Catch exahusted buffer request + catch (OutOfMemoryException) + { + return HttpStatusCode.RequestHeaderFieldsTooLarge; + } + catch (UriFormatException) + { + return HttpStatusCode.BadRequest; + } + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + private async ValueTask<ERRNO> ProcessRequestAsync(HttpContext context, HttpStatusCode status) + { + //Check status + if (status != 0) + { + /* + * If the status of the parsing was not successfull the transnport is considered + * an unknowns state and could still have data which could corrupt communications + * or worse, contatin an attack. I am choosing to drop the transport and close the + * connection if parsing the request fails + */ + //Close the connection when we exit + context.Response.Headers[HttpResponseHeader.Connection] = "closed"; + //Return status code, if the the expect header was set, return expectation failed, otherwise return the result status code + context.Respond(context.Request.Expect ? HttpStatusCode.ExpectationFailed : status); + //exit and close connection (default result will close the context) + return false; + } + //We only support version 1 and 1/1 + if ((context.Request.HttpVersion & (HttpVersion.Http11 | HttpVersion.Http1)) == 0) + { + //Close the connection when we exit + context.Response.Headers[HttpResponseHeader.Connection] = "closed"; + context.Respond(HttpStatusCode.HttpVersionNotSupported); + return false; + } + //Check open connection count (not super accurate, or might not be atomic) + if (OpenConnectionCount > Config.MaxOpenConnections) + { + //Close the connection and return 503 + context.Response.Headers[HttpResponseHeader.Connection] = "closed"; + context.Respond(HttpStatusCode.ServiceUnavailable); + return false; + } + + //Store keepalive value from request, and check if keepalives are enabled by the configuration + bool keepalive = context.Request.KeepAlive & Config.ConnectionKeepAlive > TimeSpan.Zero; + + //Set connection header (only for http1 and 1.1) + if (keepalive) + { + context.Response.Headers[HttpResponseHeader.Connection] = "keep-alive"; + context.Response.Headers[HttpResponseHeader.KeepAlive] = KeepAliveTimeoutHeaderValue; + } + else + { + //Set connection closed + context.Response.Headers[HttpResponseHeader.Connection] = "closed"; + } + //Get the server root for the specified location + if (!ServerRoots.TryGetValue(context.Request.Location.DnsSafeHost, out IWebRoot? root) && !ServerRoots.TryGetValue(WILDCARD_KEY, out root)) + { + context.Respond(HttpStatusCode.NotFound); + //make sure control leaves + return keepalive; + } + //check for redirects + if (root.Redirects.TryGetValue(context.Request.Location.LocalPath, out Redirect? r)) + { + //301 + context.Redirect301(r.RedirectUrl); + //Return keepalive + return keepalive; + } + //Check the expect header and return an early status code + if (context.Request.Expect) + { + //send a 100 status code + await context.Response.SendEarly100ContinueAsync(); + } + /* + * Initialze the request body state, which may read/buffer the request + * entity body. When doing so, the only exceptions that should be + * generated are IO, OutOfMemory, and Overflow. IOE should + * be raised to the transport as it will only be thrown if the transport + * is in an unusable state. + * + * OOM and Overflow should only be raised if an over-sized entity + * body was allowed to be read in. The Parse method should have guarded + * form data size so oom or overflow would be bugs, and we can let + * them get thrown + */ + await context.Request.InitRequestBodyAsync(Config.FormDataBufferSize, Config.HttpEncoding); + try + { + await ProcessAsync(root, context); + return keepalive; + } + //The user-code requested termination of the connection + catch (TerminateConnectionException tce) + { + //Log the event as a debug so user can see the result + Config.ServerLog.Debug(tce, "User-code requested a connection termination"); + //See if the exception requested an error code response + if (tce.Code > 0) + { + //close response with status code + context.Respond(tce.Code); + } + else + { + //Clear any currently set headers since no response is requested + context.Response.Headers.Clear(); + } + } + return false; + } + + /// <summary> + /// Processes a client connection after pre-processing has completed + /// </summary> + /// <param name="root">The <see cref="IWebRoot"/> to process the event on</param> + /// <param name="ctx">The <see cref="HttpContext"/> to process</param> + /// <returns>A task that resolves when the user-code has completed processing the entity</returns> + /// <exception cref="IOException"></exception> + /// <exception cref="TerminateConnectionException"></exception> + private static async ValueTask ProcessAsync(IWebRoot root, HttpContext ctx) + { + /* + * The event object should be cleared when it is no longer in use, IE before + * this procedure returns. + */ + HttpEvent ev = new (ctx); + try + { + await root.ClientConnectedAsync(ev); + } + //User code requested exit, elevate the exception + catch (TerminateConnectionException) + { + throw; + } + //Transport exception + catch(IOException ioe) when (ioe.InnerException is SocketException) + { + throw; + } + catch (Exception ex) + { + ctx.ParentServer.Config.ServerLog.Warn(ex, "Unhandled exception during application code execution."); + } + finally + { + ev.Clear(); + } + } + + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/IConnectionContext.cs b/Net.Http/src/Core/IConnectionContext.cs new file mode 100644 index 0000000..2e3ca46 --- /dev/null +++ b/Net.Http/src/Core/IConnectionContext.cs @@ -0,0 +1,62 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IConnectionContext.cs +* +* IConnectionContext.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Net.Http.Core +{ + /// <summary> + /// A request-response stream oriented connection state + /// </summary> + internal interface IConnectionContext + { + /// <summary> + /// Initializes the context to work with the specified + /// transport context + /// </summary> + /// <param name="tranpsort">A referrence to the transport context to use</param> + void InitializeContext(ITransportContext tranpsort); + + /// <summary> + /// Signals the context that it should prepare to process a new request + /// for the current transport + /// </summary> + void BeginRequest(); + + /// <summary> + /// Sends any pending data associated with the request to the + /// connection that begun the request + /// </summary> + /// <param name="cancellationToken">A token to cancel the operation</param> + /// <returns>A Task that completes when the response has completed</returns> + Task WriteResponseAsync(CancellationToken cancellationToken); + + /// <summary> + /// Signals to the context that it will release any request specific + /// resources + /// </summary> + void EndRequest(); + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/IHttpEvent.cs b/Net.Http/src/Core/IHttpEvent.cs new file mode 100644 index 0000000..ec1dbb5 --- /dev/null +++ b/Net.Http/src/Core/IHttpEvent.cs @@ -0,0 +1,104 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IHttpEvent.cs +* +* IHttpEvent.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Net; +using System.Collections.Generic; + +namespace VNLib.Net.Http +{ + /// <summary> + /// Contains an http request and session information. + /// </summary> + public interface IHttpEvent + { + /// <summary> + /// Current connection information. (Like "$_SERVER" superglobal in PHP) + /// </summary> + IConnectionInfo Server { get; } + /// <summary> + /// The <see cref="HttpServer"/> that this connection originated from + /// </summary> + HttpServer OriginServer { get; } + + /// <summary> + /// If the request has query arguments they are stored in key value format + /// </summary> + /// <remarks>Keys are case-insensitive</remarks> + IReadOnlyDictionary<string, string> QueryArgs { get; } + /// <summary> + /// If the request body has form data or url encoded arguments they are stored in key value format + /// </summary> + IReadOnlyDictionary<string, string> RequestArgs { get; } + /// <summary> + /// Contains all files upladed with current request + /// </summary> + /// <remarks>Keys are case-insensitive</remarks> + IReadOnlyList<FileUpload> Files { get; } + + /// <summary> + /// Complete the session and respond to user + /// </summary> + /// <param name="code">Status code of operation</param> + /// <exception cref="InvalidOperationException"></exception> + void CloseResponse(HttpStatusCode code); + + /// <summary> + /// Responds to a client with a <see cref="Stream"/> containing data to be sent to user of a given contentType. + /// Runtime will dispose of the stream during closing event + /// </summary> + /// <param name="code">Response status code</param> + /// <param name="type">MIME ContentType of data</param> + /// <param name="stream">Data to be sent to client</param> + /// <exception cref="IOException"></exception> + /// <exception cref="InvalidOperationException"></exception> + void CloseResponse(HttpStatusCode code, ContentType type, Stream stream); + + /// <summary> + /// Responds to a client with an in-memory <see cref="IMemoryResponseReader"/> containing data + /// to be sent to user of a given contentType. + /// </summary> + /// <param name="code">The status code to set</param> + /// <param name="type">The entity content-type</param> + /// <param name="entity">The in-memory response data</param> + /// <exception cref="InvalidOperationException"></exception> + void CloseResponse(HttpStatusCode code, ContentType type, IMemoryResponseReader entity); + + /// <summary> + /// Configures the server to change protocols from HTTP to the specified + /// custom protocol handler. + /// </summary> + /// <param name="protocolHandler">The custom protocol handler</param> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="InvalidOperationException"></exception> + void DangerousChangeProtocol(IAlternateProtocol protocolHandler); + + /// <summary> + /// Disables response compression + /// </summary> + void DisableCompression(); + + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/IHttpLifeCycle.cs b/Net.Http/src/Core/IHttpLifeCycle.cs new file mode 100644 index 0000000..135219d --- /dev/null +++ b/Net.Http/src/Core/IHttpLifeCycle.cs @@ -0,0 +1,62 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IHttpLifeCycle.cs +* +* IHttpLifeCycle.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +namespace VNLib.Net.Http.Core +{ + /// <summary> + /// Represents a interface of lifecycle hooks that correspond + /// with HTTP lifecycle events. + /// </summary> + internal interface IHttpLifeCycle + { + /// <summary> + /// Raised when the context is being prepare for reuse, + /// "revived from storage" + /// </summary> + void OnPrepare(); + + /// <summary> + /// Raised when the context is being released back to the pool + /// for reuse at a later time + /// </summary> + void OnRelease(); + + /// <summary> + /// Raised when a new request is about to be processed + /// on the current context + /// </summary> + void OnNewRequest(); + + /// <summary> + /// Raised when the request has been processed and the + /// response has been sent. Used to perform per-request + /// cleanup/reset for another request. + /// </summary> + /// <remarks> + /// This method is guarunteed to be called regardless of an http error, this + /// method should not throw exceptions + /// </remarks> + void OnComplete(); + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/IHttpResponseBody.cs b/Net.Http/src/Core/IHttpResponseBody.cs new file mode 100644 index 0000000..aa2dd34 --- /dev/null +++ b/Net.Http/src/Core/IHttpResponseBody.cs @@ -0,0 +1,73 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IHttpResponseBody.cs +* +* IHttpResponseBody.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + + +namespace VNLib.Net.Http.Core +{ + /// <summary> + /// Represents a rseponse entity body + /// </summary> + internal interface IHttpResponseBody + { + /// <summary> + /// A value that indicates if there is data + /// to send to the client + /// </summary> + bool HasData { get; } + + /// <summary> + /// A value that indicates if response data requires buffering + /// </summary> + bool BufferRequired { get; } + + /// <summary> + /// Writes internal response entity data to the destination stream + /// </summary> + /// <param name="dest">The response stream to write data to</param> + /// <param name="buffer">An optional buffer used to buffer responses</param> + /// <param name="count">The maximum length of the response data to write</param> + /// <param name="token">A token to cancel the operation</param> + /// <returns>A task that resolves when the response is completed</returns> + Task WriteEntityAsync(Stream dest, long count, Memory<byte>? buffer, CancellationToken token); + + /// <summary> + /// Writes internal response entity data to the destination stream + /// </summary> + /// <param name="dest">The response stream to write data to</param> + /// <param name="buffer">An optional buffer used to buffer responses</param> + /// <param name="token">A token to cancel the operation</param> + /// <returns>A task that resolves when the response is completed</returns> + Task WriteEntityAsync(Stream dest, Memory<byte>? buffer, CancellationToken token); + + /// <summary> + /// The length of the content + /// </summary> + long Length { get; } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/Request/HttpInputStream.cs b/Net.Http/src/Core/Request/HttpInputStream.cs new file mode 100644 index 0000000..61d215f --- /dev/null +++ b/Net.Http/src/Core/Request/HttpInputStream.cs @@ -0,0 +1,222 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpInputStream.cs +* +* HttpInputStream.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Buffers; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Utils; +using VNLib.Utils.IO; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; + +namespace VNLib.Net.Http.Core +{ + /// <summary> + /// Specialized stream to allow reading a request entity body with a fixed content length. + /// </summary> + internal sealed class HttpInputStream : Stream + { + private readonly Func<Stream> GetTransport; + + private long ContentLength; + private Stream? InputStream; + private long _position; + + private ISlindingWindowBuffer<byte>? _initalData; + + public HttpInputStream(Func<Stream> getTransport) => GetTransport = getTransport; + + internal void OnComplete() + { + //Dispose the inigial data buffer + _initalData?.Close(); + _initalData = null; + //Remove stream cache copy + InputStream = null; + //Reset position + _position = 0; + //reset content length + ContentLength = 0; + } + + /// <summary> + /// Creates a new input stream object configured to allow reading of the specified content length + /// bytes from the stream and consumes the initial buffer to read data from on initial read calls + /// </summary> + /// <param name="contentLength">The number of bytes to allow being read from the transport or initial buffer</param> + /// <param name="initial">Entity body data captured on initial read</param> + internal void Prepare(long contentLength, ISlindingWindowBuffer<byte>? initial) + { + ContentLength = contentLength; + _initalData = initial; + + //Cache transport + InputStream = GetTransport(); + } + + public override void Close() => throw new NotSupportedException("The HTTP input stream should never be closed!"); + private long Remaining => Math.Max(ContentLength - _position, 0); + public override bool CanRead => true; + public override bool CanSeek => true; + public override bool CanWrite => false; + public override long Length => ContentLength; + public override long Position { get => _position; set { } } + + public override void Flush(){} + + public override int Read(byte[] buffer, int offset, int count) => Read(buffer.AsSpan(offset, count)); + public override int Read(Span<byte> buffer) + { + //Calculate the amount of data that can be read into the buffer + int bytesToRead = (int)Math.Min(buffer.Length, Remaining); + if (bytesToRead == 0) + { + return 0; + } + + //Clamp output buffer size and create buffer writer + ForwardOnlyWriter<byte> writer = new(buffer[..bytesToRead]); + + //See if all data is internally buffered + if (_initalData != null && _initalData.AccumulatedSize > 0) + { + //Read as much as possible from internal buffer + ERRNO read = _initalData.Read(writer.Remaining); + + //Advance writer + writer.Advance(read); + + //Update position + _position += read; + } + + //See if data is still remaining to be read from transport (reamining size is also the amount of data that can be read) + if (writer.RemainingSize > 0) + { + //Read from transport + ERRNO read = InputStream!.Read(writer.Remaining); + + //Update writer position + writer.Advance(read); + + _position += read; + } + + //Return number of bytes written to the buffer + return writer.Written; + } + + public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + } + public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) + { + //Calculate the amount of data that can be read into the buffer + int bytesToRead = (int)Math.Min(buffer.Length, Remaining); + if (bytesToRead == 0) + { + return 0; + } + + //Clamp output buffer size and create buffer writer + ForwardOnlyMemoryWriter<byte> writer = new(buffer[..bytesToRead]); + + //See if all data is internally buffered + if (_initalData != null && _initalData.AccumulatedSize > 0) + { + //Read as much as possible from internal buffer + ERRNO read = _initalData.Read(writer.Remaining.Span); + + //Advance writer + writer.Advance(read); + + //Update position + _position += read; + } + + //See if data is still remaining to be read from transport (reamining size is also the amount of data that can be read) + if (writer.RemainingSize > 0) + { + //Read from transport + ERRNO read = await InputStream!.ReadAsync(writer.Remaining, cancellationToken).ConfigureAwait(false); + + //Update writer position + writer.Advance(read); + + _position += read; + } + + //Return number of bytes written to the buffer + return writer.Written; + } + + /// <summary> + /// Asynchronously discards all remaining data in the stream + /// </summary> + /// <param name="heap">The heap to alloc buffers from</param> + /// <param name="maxBufferSize">The maxium size of the buffer to allocate</param> + /// <returns>A task that represents the discard operations</returns> + public async ValueTask DiscardRemainingAsync(int maxBufferSize) + { + long remaining = Remaining; + if(remaining == 0) + { + return; + } + //See if all data has already been buffered + if(_initalData != null && remaining <= _initalData.AccumulatedSize) + { + //All data has been buffred, so just clear the buffer + _position = Length; + } + //We must actaully disacrd data from the stream + else + { + //Calcuate a buffer size to allocate (will never be larger than an int) + int bufferSize = (int)Math.Min(remaining, maxBufferSize); + //Alloc a discard buffer to reset the transport + using IMemoryOwner<byte> discardBuffer = CoreBufferHelpers.GetMemory(bufferSize, false); + int read = 0; + do + { + //Read data to the discard buffer until reading is completed + read = await ReadAsync(discardBuffer.Memory, CancellationToken.None).ConfigureAwait(true); + + } while (read != 0); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + //Ignore seek control + return _position; + } + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/Request/HttpRequest.cs b/Net.Http/src/Core/Request/HttpRequest.cs new file mode 100644 index 0000000..2410a8f --- /dev/null +++ b/Net.Http/src/Core/Request/HttpRequest.cs @@ -0,0 +1,284 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpRequest.cs +* +* HttpRequest.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Net; +using System.Collections.Generic; +using System.Security.Authentication; + +using VNLib.Utils; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; + +namespace VNLib.Net.Http.Core +{ + + internal class HttpRequest : IHttpLifeCycle +#if DEBUG + ,IStringSerializeable +#endif + { + public readonly VnWebHeaderCollection Headers; + public readonly Dictionary<string, string> Cookies; + public readonly List<string> Accept; + public readonly List<string> AcceptLanguage; + public readonly HttpRequestBody RequestBody; + + public HttpVersion HttpVersion { get; set; } + public HttpMethod Method { get; set; } + public string? UserAgent { get; set; } + public string? Boundry { get; set; } + public ContentType ContentType { get; set; } + public string? Charset { get; set; } + public Uri Location { get; set; } + public Uri? Origin { get; set; } + public Uri? Referrer { get; set; } + internal bool KeepAlive { get; set; } + public IPEndPoint RemoteEndPoint { get; set; } + public IPEndPoint LocalEndPoint { get; set; } + public SslProtocols EncryptionVersion { get; set; } + public Tuple<long, long>? Range { get; set; } + /// <summary> + /// A value indicating whether the connection contained a request entity body. + /// </summary> + public bool HasEntityBody { get; set; } + /// <summary> + /// A transport stream wrapper that is positioned for reading + /// the entity body from the input stream + /// </summary> + public HttpInputStream InputStream { get; } + /// <summary> + /// A value indicating if the client's request had an Expect-100-Continue header + /// </summary> + public bool Expect { get; set; } + +#nullable disable + public HttpRequest(Func<Stream> getTransport) + { + //Create new collection for headers + Headers = new(); + //Create new collection for request cookies + Cookies = new(); + //New list for accept + Accept = new(); + AcceptLanguage = new(); + //New reusable input stream + InputStream = new(getTransport); + RequestBody = new(); + } + + + public void OnPrepare() + {} + + public void OnRelease() + {} + + public void OnNewRequest() + { + //Set to defaults + ContentType = ContentType.NonSupported; + EncryptionVersion = default; + Method = HttpMethod.NOT_SUPPORTED; + HttpVersion = HttpVersion.NotSupported; + } + + public void OnComplete() + { + //release the input stream + InputStream.OnComplete(); + RequestBody.OnComplete(); + //Make sure headers, cookies, and accept are cleared for reuse + Headers.Clear(); + Cookies.Clear(); + Accept.Clear(); + AcceptLanguage.Clear(); + //Clear request flags + this.Expect = false; + this.KeepAlive = false; + this.HasEntityBody = false; + //We need to clean up object refs + this.Boundry = default; + this.Charset = default; + this.LocalEndPoint = default; + this.Location = default; + this.Origin = default; + this.Referrer = default; + this.RemoteEndPoint = default; + this.UserAgent = default; + this.Range = default; + //We are all set to reuse the instance + } + + +#if DEBUG + public string Compile() + { + //Alloc char buffer for compilation + using UnsafeMemoryHandle<char> buffer = Memory.UnsafeAlloc<char>(16 * 1024, true); + ForwardOnlyWriter<char> writer = new(buffer.Span); + Compile(ref writer); + return writer.ToString(); + } + + public void Compile(ref ForwardOnlyWriter<char> writer) + { + //Request line + writer.Append(Method.ToString()); + writer.Append(" "); + writer.Append(Location?.PathAndQuery); + writer.Append(" HTTP/"); + switch (HttpVersion) + { + case HttpVersion.NotSupported: + writer.Append("Unsuppored Http version"); + break; + case HttpVersion.Http1: + writer.Append("1.0"); + break; + case HttpVersion.Http11: + writer.Append("1.1"); + break; + case HttpVersion.Http2: + writer.Append("2.0"); + break; + case HttpVersion.Http09: + writer.Append("0.9"); + break; + } + writer.Append("\r\n"); + //write host + writer.Append("Host: "); + writer.Append(Location?.Authority); + writer.Append("\r\n"); + + //Write headers + foreach (string header in Headers.Keys) + { + writer.Append(header); + writer.Append(": "); + writer.Append(Headers[header]); + writer.Append("\r\n"); + } + //Write cookies + foreach (string cookie in Cookies.Keys) + { + writer.Append("Cookie: "); + writer.Append(cookie); + writer.Append("="); + writer.Append(Cookies[cookie]); + writer.Append("\r\n"); + } + + //Write accept + if (Accept.Count > 0) + { + writer.Append("Accept: "); + foreach (string accept in Accept) + { + writer.Append(accept); + writer.Append(", "); + } + writer.Append("\r\n"); + } + //Write accept language + if (AcceptLanguage.Count > 0) + { + writer.Append("Accept-Language: "); + foreach (string acceptLanguage in AcceptLanguage) + { + writer.Append(acceptLanguage); + writer.Append(", "); + } + writer.Append("\r\n"); + } + //Write user agent + if (UserAgent != null) + { + writer.Append("User-Agent: "); + writer.Append(UserAgent); + writer.Append("\r\n"); + } + //Write content type + if (ContentType != ContentType.NonSupported) + { + writer.Append("Content-Type: "); + writer.Append(HttpHelpers.GetContentTypeString(ContentType)); + writer.Append("\r\n"); + } + //Write content length + if (ContentType != ContentType.NonSupported) + { + writer.Append("Content-Length: "); + writer.Append(InputStream.Length); + writer.Append("\r\n"); + } + if (KeepAlive) + { + writer.Append("Connection: keep-alive\r\n"); + } + if (Expect) + { + writer.Append("Expect: 100-continue\r\n"); + } + if(Origin != null) + { + writer.Append("Origin: "); + writer.Append(Origin.ToString()); + writer.Append("\r\n"); + } + if (Referrer != null) + { + writer.Append("Referrer: "); + writer.Append(Referrer.ToString()); + writer.Append("\r\n"); + } + writer.Append("from "); + writer.Append(RemoteEndPoint.ToString()); + writer.Append("\r\n"); + writer.Append("Received on "); + writer.Append(LocalEndPoint.ToString()); + //Write end of headers + writer.Append("\r\n"); + } + + public ERRNO Compile(in Span<char> buffer) + { + ForwardOnlyWriter<char> writer = new(buffer); + Compile(ref writer); + return writer.Written; + } + public override string ToString() + { + return Compile(); + } +#else + public override string ToString() + { + return "Request debug output only available when compiled in DEBUG mode"; + } +#endif + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/Request/HttpRequestBody.cs b/Net.Http/src/Core/Request/HttpRequestBody.cs new file mode 100644 index 0000000..824ca24 --- /dev/null +++ b/Net.Http/src/Core/Request/HttpRequestBody.cs @@ -0,0 +1,70 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpRequestBody.cs +* +* HttpRequestBody.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Collections.Generic; + +namespace VNLib.Net.Http.Core +{ + /// <summary> + /// Represents a higher-level request entity body (query arguments, request body etc) + /// that has been parsed and captured + /// </summary> + internal class HttpRequestBody + { + public readonly List<FileUpload> Uploads; + public readonly Dictionary<string, string> RequestArgs; + public readonly Dictionary<string, string> QueryArgs; + + public HttpRequestBody() + { + Uploads = new(1); + + //Request/query args should not be request sensitive + RequestArgs = new(StringComparer.OrdinalIgnoreCase); + QueryArgs = new(StringComparer.OrdinalIgnoreCase); + } + + /// <summary> + /// Releases all resources used by the current instance + /// </summary> + public void OnComplete() + { + //Only enumerate/clear if file uplaods are present + if (Uploads.Count > 0) + { + //Dispose all initialized files + for (int i = 0; i < Uploads.Count; i++) + { + Uploads[i].Free(); + } + //Emtpy list + Uploads.Clear(); + } + //Clear request args and file uplaods + RequestArgs.Clear(); + QueryArgs.Clear(); + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/Request/HttpRequestExtensions.cs b/Net.Http/src/Core/Request/HttpRequestExtensions.cs new file mode 100644 index 0000000..6a93192 --- /dev/null +++ b/Net.Http/src/Core/Request/HttpRequestExtensions.cs @@ -0,0 +1,304 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpRequestExtensions.cs +* +* HttpRequestExtensions.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; + +using static VNLib.Net.Http.Core.CoreBufferHelpers; + +namespace VNLib.Net.Http.Core +{ + internal static class HttpRequestExtensions + { + public enum CompressionType + { + None, + Gzip, + Deflate, + Brotli + } + + /// <summary> + /// Gets the <see cref="CompressionType"/> that the connection accepts + /// in a default order, or none if not enabled + /// </summary> + /// <param name="request"></param> + /// <returns>A <see cref="CompressionType"/> with a value the connection support</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CompressionType GetCompressionSupport(this HttpRequest request) + { + string? acceptEncoding = request.Headers[HttpRequestHeader.AcceptEncoding]; + + if (acceptEncoding == null) + { + return CompressionType.None; + } + else if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase)) + { + return CompressionType.Gzip; + } + else if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase)) + { + return CompressionType.Deflate; + } + else if (acceptEncoding.Contains("br", StringComparison.OrdinalIgnoreCase)) + { + return CompressionType.Brotli; + } + else + { + return CompressionType.None; + } + } + + + /// <summary> + /// Tests the connection's origin header against the location URL by authority. + /// An origin matches if its scheme, host, and port match + /// </summary> + /// <returns>true if the origin header was set and does not match the current locations origin</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsCrossOrigin(this HttpRequest Request) + { + return Request.Origin != null + && (!Request.Origin.Authority.Equals(Request.Location.Authority, StringComparison.Ordinal) + || !Request.Origin.Scheme.Equals(Request.Location.Scheme, StringComparison.Ordinal)); + } + /// <summary> + /// Is the current connection a websocket upgrade request handshake + /// </summary> + /// <returns>true if the connection is a websocket upgrade request, false otherwise</returns> + public static bool IsWebSocketRequest(this HttpRequest Request) + { + string? upgrade = Request.Headers[HttpRequestHeader.Upgrade]; + if (!string.IsNullOrWhiteSpace(upgrade) && upgrade.Contains("websocket", StringComparison.OrdinalIgnoreCase)) + { + //This request is a websocket request + //Check connection header + string? connection = Request.Headers[HttpRequestHeader.Connection]; + //Must be a web socket request + return !string.IsNullOrWhiteSpace(connection) && connection.Contains("upgrade", StringComparison.OrdinalIgnoreCase); + } + return false; + } + + /// <summary> + /// Initializes the <see cref="HttpRequest"/> for an incomming connection + /// </summary> + /// <param name="server"></param> + /// <param name="ctx">The <see cref="TransportEventContext"/> to attach the request to</param> + /// <param name="defaultHttpVersion">The default http version</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Initialize(this HttpRequest server, ITransportContext ctx, HttpVersion defaultHttpVersion) + { + server.LocalEndPoint = ctx.LocalEndPoint; + server.RemoteEndPoint = ctx.RemoteEndpoint; + server.EncryptionVersion = ctx.SslVersion; + //Set to default http version so the response can be configured properly + server.HttpVersion = defaultHttpVersion; + } + + + /// <summary> + /// Initializes the <see cref="HttpRequest.RequestBody"/> for the current request + /// </summary> + /// <param name="Request"></param> + /// <param name="maxBufferSize">The maxium buffer size allowed while parsing reqeust body data</param> + /// <param name="encoding">The request data encoding for url encoded or form data bodies</param> + /// <exception cref="IOException"></exception> + /// <exception cref="OverflowException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + internal static ValueTask InitRequestBodyAsync(this HttpRequest Request, int maxBufferSize, Encoding encoding) + { + /* + * Parses query parameters from the request location query + */ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void ParseQueryArgs(HttpRequest Request) + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + //Query string parse method + static void QueryParser(ReadOnlySpan<char> queryArgument, HttpRequest Request) + { + //Split spans after the '=' character + ReadOnlySpan<char> key = queryArgument.SliceBeforeParam('='); + ReadOnlySpan<char> value = queryArgument.SliceAfterParam('='); + //Insert into dict + Request.RequestBody.QueryArgs[key.ToString()] = value.ToString(); + } + + //if the request has query args, parse and store them + ReadOnlySpan<char> queryString = Request.Location.Query; + if (!queryString.IsEmpty) + { + //trim leading '?' if set + queryString = queryString.TrimStart('?'); + //Split args by '&' + queryString.Split('&', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries, QueryParser, Request); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static async ValueTask ParseInputStream(HttpRequest Request, int maxBufferSize, Encoding encoding) + { + /* + * Reads all available data from the request input stream + * If the stream size is smaller than a TCP buffer size, will complete synchronously + */ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static ValueTask<VnString> ReadInputStreamAsync(HttpRequest Request, int maxBufferSize, Encoding encoding) + { + //Calculate a largest available buffer to read the entire stream or up to the maximum buffer size + int bufferSize = (int)Math.Min(Request.InputStream.Length, maxBufferSize); + //Read the stream into a vnstring + return VnString.FromStreamAsync(Request.InputStream, encoding, HttpPrivateHeap, bufferSize); + } + /* + * SpanSplit callback function for storing UrlEncoded request entity + * bodies in key-value pairs and writing them to the + */ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void UrlEncodedSplitCb(ReadOnlySpan<char> kvArg, HttpRequest Request) + { + //Get key side of agument (or entire argument if no value is set) + ReadOnlySpan<char> key = kvArg.SliceBeforeParam('='); + ReadOnlySpan<char> value = kvArg.SliceAfterParam('='); + //trim, allocate strings, and store in the request arg dict + Request.RequestBody.RequestArgs[key.TrimCRLF().ToString()] = value.TrimCRLF().ToString(); + } + + /* + * Parses a Form-Data content type request entity body and stores those arguments in + * Request uploads or request args + */ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void FormDataBodySplitCb(ReadOnlySpan<char> formSegment, ValueTuple<HttpRequestBody, Encoding> state) + { + //Form data arguments + string? DispType = null, Name = null, FileName = null; + ContentType ctHeaderVal = ContentType.NonSupported; + //Get sliding window for parsing data + ForwardOnlyReader<char> reader = new(formSegment.TrimCRLF()); + //Read content headers + do + { + //Get the index of the next crlf + int index = reader.Window.IndexOf(HttpHelpers.CRLF); + //end of headers + if (index < 1) + { + break; + } + //Get header data + ReadOnlySpan<char> header = reader.Window[..index]; + //Split header at colon + int colon = header.IndexOf(':'); + //If no data is available after the colon the header is not valid, so move on to the next body + if (colon < 1) + { + return; + } + //Hash the header value into a header enum + HttpRequestHeader headerType = HttpHelpers.GetRequestHeaderEnumFromValue(header[..colon]); + //get the header value + ReadOnlySpan<char> headerValue = header[(colon + 1)..]; + //Check for content dispositon header + if (headerType == HttpHelpers.ContentDisposition) + { + //Parse the content dispostion + HttpHelpers.ParseDisposition(headerValue, out DispType, out Name, out FileName); + } + //Check for content type + else if (headerType == HttpRequestHeader.ContentType) + { + //The header value for content type should be an MIME content type + ctHeaderVal = HttpHelpers.GetContentType(headerValue.Trim().ToString()); + } + //Shift window to the next line + reader.Advance(index + HttpHelpers.CRLF.Length); + } while (true); + //Remaining data should be the body data (will have leading and trailing CRLF characters + //If filename is set, this must be a file + if (!string.IsNullOrWhiteSpace(FileName)) + { + //Store the file in the uploads + state.Item1.Uploads.Add(FileUpload.FromString(reader.Window.TrimCRLF(), state.Item2, FileName, ctHeaderVal)); + } + //Make sure the name parameter was set and store the message body as a string + else if (!string.IsNullOrWhiteSpace(Name)) + { + //String data as body + state.Item1.RequestArgs[Name] = reader.Window.TrimCRLF().ToString(); + } + } + + switch (Request.ContentType) + { + //CT not supported, dont read it + case ContentType.NonSupported: + break; + case ContentType.UrlEncoded: + //Create a vnstring from the message body and parse it (assuming url encoded bodies are small so a small stack buffer will be fine) + using (VnString urlbody = await ReadInputStreamAsync(Request, maxBufferSize, encoding)) + { + //Get the body as a span, and split the 'string' at the & character + urlbody.AsSpan().Split('&', StringSplitOptions.RemoveEmptyEntries, UrlEncodedSplitCb, Request); + } + break; + case ContentType.MultiPart: + //Make sure we have a boundry specified + if (string.IsNullOrWhiteSpace(Request.Boundry)) + { + break; + } + //Read all data from stream into string + using (VnString body = await ReadInputStreamAsync(Request, maxBufferSize, encoding)) + { + //Split the body as a span at the boundries + body.AsSpan().Split($"--{Request.Boundry}", StringSplitOptions.RemoveEmptyEntries, FormDataBodySplitCb, (Request.RequestBody, encoding)); + } + break; + //Default case is store as a file + default: + //add upload + Request.RequestBody.Uploads.Add(new(Request.InputStream, string.Empty, Request.ContentType, false)); + break; + } + } + + //Parse query + ParseQueryArgs(Request); + + //Decode requests from body + return !Request.HasEntityBody ? ValueTask.CompletedTask : ParseInputStream(Request, maxBufferSize, encoding); + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs b/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs new file mode 100644 index 0000000..b37b78b --- /dev/null +++ b/Net.Http/src/Core/RequestParse/Http11ParseExtensions.cs @@ -0,0 +1,533 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: Http11ParseExtensions.cs +* +* Http11ParseExtensions.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Net; +using System.Linq; +using System.Collections.Generic; +using System.Security.Authentication; +using System.Runtime.CompilerServices; + +using VNLib.Utils; +using VNLib.Utils.IO; +using VNLib.Utils.Logging; +using VNLib.Utils.Extensions; + +namespace VNLib.Net.Http.Core +{ + + internal static class Http11ParseExtensions + { + + /// <summary> + /// Stores the state of an HTTP/1.1 parsing operation + /// </summary> + public ref struct Http1ParseState + { + internal UriBuilder? Location; + internal bool IsAbsoluteRequestUrl; + internal long ContentLength; + } + + + /// <summary> + /// Reads the first line from the transport stream using the specified buffer + /// and parses the HTTP request line components: Method, resource, Http Version + /// </summary> + /// <param name="Request"></param> + /// <param name="reader">The reader to read lines from the transport</param> + /// <param name="parseState">The HTTP1 parsing state</param> + /// <param name="lineBuf">The buffer to use when parsing the request data</param> + /// <returns>0 if the request line was successfully parsed, a status code if the request could not be processed</returns> + /// <exception cref="UriFormatException"></exception> + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static HttpStatusCode Http1ParseRequestLine(this HttpRequest Request, ref Http1ParseState parseState, ref TransportReader reader, in Span<char> lineBuf) + { + //Locals + ERRNO requestResult; + int index, endloc; + + //Read the start line + requestResult = reader.ReadLine(lineBuf); + //Must be able to parse the verb and location + if (requestResult < 1) + { + //empty request + return (HttpStatusCode)1000; + } + + //true up the request line to actual size + ReadOnlySpan<char> requestLine = lineBuf[..(int)requestResult].Trim(); + //Find the first white space character ("GET / HTTP/1.1") + index = requestLine.IndexOf(' '); + if (index == -1) + { + return HttpStatusCode.BadRequest; + } + + //Decode the verb (function requires the string be the exact characters of the request method) + Request.Method = HttpHelpers.GetRequestMethod(requestLine[0..index]); + //Make sure the method is supported + if (Request.Method == HttpMethod.NOT_SUPPORTED) + { + return HttpStatusCode.MethodNotAllowed; + } + + //location string should be from end of verb to HTTP/ NOTE: Only supports http... this is an http server + endloc = requestLine.LastIndexOf(" HTTP/", StringComparison.OrdinalIgnoreCase); + //Client must specify an http version prepended by a single whitespace(rfc2612) + if (endloc == -1) + { + return HttpStatusCode.HttpVersionNotSupported; + } + + //Try to parse the version and only accept the 3 major versions of http + Request.HttpVersion = HttpHelpers.ParseHttpVersion(requestLine[endloc..]); + //Check to see if the version was parsed succesfully + if (Request.HttpVersion == HttpVersion.NotSupported) + { + //Return not supported + return HttpStatusCode.HttpVersionNotSupported; + } + + //Set keepalive flag if http11 + Request.KeepAlive = Request.HttpVersion == HttpVersion.Http11; + + //Get the location segment from the request line + ReadOnlySpan<char> paq = requestLine[(index + 1)..endloc].TrimCRLF(); + + //Process an absolute uri, + if (paq.Contains("://", StringComparison.Ordinal)) + { + //Convert the location string to a .net string and init the location builder (will perform validation when the Uri propery is used) + parseState.Location = new(paq.ToString()); + parseState.IsAbsoluteRequestUrl = true; + return 0; + } + //Try to capture a realative uri + else if (paq.Length > 0 && paq[0] == '/') + { + //Create a default location uribuilder + parseState.Location = new() + { + //Set a default scheme + Scheme = Request.EncryptionVersion == SslProtocols.None ? Uri.UriSchemeHttp : Uri.UriSchemeHttps, + }; + //Need to manually parse the query string + int q = paq.IndexOf('?'); + //has query? + if (q == -1) + { + parseState.Location.Path = paq.ToString(); + } + //Does have query argument + else + { + //separate the path from the query + parseState.Location.Path = paq[0..q].ToString(); + parseState.Location.Query = paq[(q + 1)..].ToString(); + } + return 0; + } + //Cannot service an unknonw location + return HttpStatusCode.BadRequest; + } + + /// <summary> + /// Reads headers from the transport using the supplied character buffer, and updates the current request + /// </summary> + /// <param name="Request"></param> + /// <param name="parseState">The HTTP1 parsing state</param> + /// <param name="Config">The current server <see cref="HttpConfig"/></param> + /// <param name="reader">The <see cref="VnStreamReader"/> to read lines from the transport</param> + /// <param name="lineBuf">The buffer read data from the transport with</param> + /// <returns>0 if the request line was successfully parsed, a status code if the request could not be processed</returns> + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static HttpStatusCode Http1ParseHeaders(this HttpRequest Request, ref Http1ParseState parseState, ref TransportReader reader, in HttpConfig Config, in Span<char> lineBuf) + { + try + { + int headerCount = 0, colon; + bool hostFound = false; + ERRNO charsRead; + ReadOnlySpan<char> headerName, requestHeaderValue; + + /* + * This loop will read "lines" from the transport/reader buffer as headers + * and store them in the rented character buffer with 0 allocations. + * + * Lines will be read from the transport reader until an empty line is read, + * or an exception occurs. The VnStreamReader class will search for lines + * directly in the binary rather than converting the data then parsing it. + * When a line is parsed, its assumed to be an HTTP header at this point in + * the parsing, and is separated into its key-value pair determined by the + * first ':' character to appear. + * + * The header length will be limited by the size of the character buffer, + * or the reader binary buffer while reading lines. Buffer sizes are fixed + * to the system memory page size. Depending on the encoding the user chooses + * this should not be an issue for most configurations. This strategy is + * most efficient for realtivly small header sizes. + * + * The header's key is hashed by the HttpHelpers class and the hash is used to + * index a lookup table to return its enumeration value which is used in the swtich + * statement to reduce the number of strings added to the request header container. + * This was a major effort to reduce memory and CPU overhead while using the + * WebHeaderCollection .NET class, which I think is still worth using instead of a + * custom header data structure class. + * + * Some case statments are custom HttpRequestHeader enum values via internal casted + * constants to be consistant with he .NET implementation. + */ + do + { + //Read a line until we reach the end of headers, this call will block if end of characters is reached and a new string will be read + charsRead = reader.ReadLine(lineBuf); + + //If the result is less than 1, no line is available (end of headers) or could not be read + if (charsRead < 1) + { + break; + } + + //Header count exceeded or header larger than header buffer size + if (charsRead < 0 || headerCount > Config.MaxRequestHeaderCount) + { + return HttpStatusCode.RequestHeaderFieldsTooLarge; + } + + { + //Get the true size of the read header line as a readonly span + ReadOnlySpan<char> header = lineBuf[..(int)charsRead]; + + /* + * RFC 7230, ignore headers with preceeding whitespace + * + * If the first character is whitespace that is enough to + * ignore the rest of the header + */ + if (header[0] == ' ') + { + //Move on to next header + continue; + } + + //Find the first colon + colon = header.IndexOf(':'); + //No colon was found, this is an invalid string, try to skip it and keep reading + if (colon <= 0) + { + continue; + } + + //Store header and its value (sections before and after colon) + headerName = header[..colon].TrimCRLF(); + requestHeaderValue = header[(colon + 1)..].TrimCRLF(); + } + + //Hash the header key and lookup the request header value + switch (HttpHelpers.GetRequestHeaderEnumFromValue(headerName)) + { + case HttpRequestHeader.Connection: + { + //Update keepalive, if the connection header contains "closed" and with the current value of keepalive + Request.KeepAlive &= !requestHeaderValue.Contains("close", StringComparison.OrdinalIgnoreCase); + //Also store the connecion header into the store + Request.Headers.Add(HttpRequestHeader.Connection, requestHeaderValue.ToString()); + } + break; + case HttpRequestHeader.ContentType: + { + if (!HttpHelpers.TryParseContentType(requestHeaderValue.ToString(), out string? ct, out string? charset, out string? boundry) || ct == null) + { + //Invalid content type header value + return HttpStatusCode.UnsupportedMediaType; + } + Request.Boundry = boundry; + Request.Charset = charset; + //Get the content type enum from mime type + Request.ContentType = HttpHelpers.GetContentType(ct); + } + break; + case HttpRequestHeader.ContentLength: + { + //Content length has already been calculated, ERROR, rfc 7230 + if(parseState.ContentLength > 0) + { + Config.ServerLog.Debug("Message warning, recieved multiple content length headers"); + return HttpStatusCode.BadRequest; + } + + //Only capture positive values, and if length is negative we are supposed to ignore it + if (ulong.TryParse(requestHeaderValue, out ulong len) && len < long.MaxValue) + { + parseState.ContentLength = (long)len; + } + else + { + return HttpStatusCode.BadRequest; + } + + //Request size it too large to service + if (parseState.ContentLength > Config.MaxUploadSize) + { + return HttpStatusCode.RequestEntityTooLarge; + } + } + break; + case HttpRequestHeader.Host: + { + //Set host found flag + hostFound = true; + + //Split the host value by the port parameter + ReadOnlySpan<char> port = requestHeaderValue.SliceAfterParam(':').Trim(); + //Slicing beofre the colon should always provide a useable hostname, so allocate a string for it + string host = requestHeaderValue.SliceBeforeParam(':').Trim().ToString(); + + //Verify that the host is usable + if (Uri.CheckHostName(host) == UriHostNameType.Unknown) + { + return HttpStatusCode.BadRequest; + } + + //Verify that the host matches the host header if absolue uri is set + if (parseState.IsAbsoluteRequestUrl) + { + if (!host.Equals(parseState.Location!.Host, StringComparison.OrdinalIgnoreCase)) + { + return HttpStatusCode.BadRequest; + } + } + + //store the host value + parseState.Location!.Host = host; + + //If the port span is empty, no colon was found or the port is invalid + if (!port.IsEmpty) + { + //try to parse the port number + if (!int.TryParse(port, out int p) || p < 0 || p > ushort.MaxValue) + { + return HttpStatusCode.BadRequest; + } + //Store port + parseState.Location.Port = p; + } + } + break; + case HttpRequestHeader.Cookie: + { + //Local function to break cookie segments into key-value pairs + static void AddCookiesCallback(ReadOnlySpan<char> cookie, Dictionary<string, string> cookieContainer) + { + //Get the name parameter and alloc a string + string name = cookie.SliceBeforeParam('=').Trim().ToString(); + //Get the value parameter and alloc a string + string value = cookie.SliceAfterParam('=').Trim().ToString(); + //Add the cookie to the dictionary + _ = cookieContainer.TryAdd(name, value); + } + //Split all cookies by ; with trailing whitespace + requestHeaderValue.Split("; ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries, AddCookiesCallback, Request.Cookies); + } + break; + case HttpRequestHeader.AcceptLanguage: + //Capture accept languages and store in the request accept collection + requestHeaderValue.Split(',', Request.AcceptLanguage, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + break; + case HttpRequestHeader.Accept: + //Capture accept content types and store in request accept collection + requestHeaderValue.Split(',', Request.Accept, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + break; + case HttpRequestHeader.Referer: + { + //Check the referer header and capture its uri instance, it should be absolutely parseable + if (!requestHeaderValue.IsEmpty && Uri.TryCreate(requestHeaderValue.ToString(), UriKind.Absolute, out Uri? refer)) + { + Request.Referrer = refer; + } + } + break; + case HttpRequestHeader.Range: + { + //See if range bytes value has been set + ReadOnlySpan<char> rawRange = requestHeaderValue.SliceAfterParam("bytes=").TrimCRLF(); + //Make sure the bytes parameter is set + if (rawRange.IsEmpty) + { + break; + } + //Get start range + ReadOnlySpan<char> startRange = rawRange.SliceBeforeParam('-'); + //Get end range (empty if no - exists) + ReadOnlySpan<char> endRange = rawRange.SliceAfterParam('-'); + //See if a range end is specified + if (endRange.IsEmpty) + { + //No end range specified, so only range start + if (long.TryParse(startRange, out long start) && start > -1) + { + //Create new range + Request.Range = new(start, -1); + break; + } + } + //Range has a start and end + else if (long.TryParse(startRange, out long start) && long.TryParse(endRange, out long end) && end > -1) + { + //get start and end components from range header + Request.Range = new(start, end); + break; + } + } + //Could not parse start range from header + return HttpStatusCode.RequestedRangeNotSatisfiable; + case HttpRequestHeader.UserAgent: + //Store user-agent + Request.UserAgent = requestHeaderValue.IsEmpty ? string.Empty : requestHeaderValue.TrimCRLF().ToString(); + break; + //Special code for origin header + case HttpHelpers.Origin: + { + //Alloc a string for origin + string origin = requestHeaderValue.ToString(); + //Origin headers should always be absolute address "parsable" + if (Uri.TryCreate(origin, UriKind.Absolute, out Uri? org)) + { + Request.Origin = org; + } + } + break; + case HttpRequestHeader.Expect: + //Accept 100-continue for the Expect header value + Request.Expect = requestHeaderValue.Equals("100-continue", StringComparison.OrdinalIgnoreCase); + break; + default: + //By default store the header in the request header store + Request.Headers.Add(headerName.ToString(), requestHeaderValue.ToString()); + break; + } + //Increment header count + headerCount++; + } while (true); + + //If request is http11 then host is required + if (Request.HttpVersion == HttpVersion.Http11 && !hostFound) + { + return HttpStatusCode.BadRequest; + } + + } + //Catch an arugment exception within the header add function to cause a bad request result + catch (ArgumentException) + { + return HttpStatusCode.BadRequest; + } + + //Check the final location to make sure data was properly sent + if (string.IsNullOrWhiteSpace(parseState.Location?.Host) + || string.IsNullOrWhiteSpace(parseState.Location.Scheme) + || string.IsNullOrWhiteSpace(parseState.Location.Path) + ) + { + return HttpStatusCode.BadRequest; + } + + //Store the finalized location + Request.Location = parseState.Location.Uri; + + return 0; + } + /// <summary> + /// Prepares the entity body for the current HTTP1 request + /// </summary> + /// <param name="Request"></param> + /// <param name="Config">The current server <see cref="HttpConfig"/></param> + /// <param name="parseState">The HTTP1 parsing state</param> + /// <param name="reader">The <see cref="VnStreamReader"/> to read lines from the transport</param> + /// <returns>0 if the request line was successfully parsed, a status code if the request could not be processed</returns> + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static HttpStatusCode Http1PrepareEntityBody(this HttpRequest Request, ref Http1ParseState parseState, ref TransportReader reader, in HttpConfig Config) + { + //If the content type is multipart, make sure its not too large to ingest + if (Request.ContentType == ContentType.MultiPart && parseState.ContentLength > Config.MaxFormDataUploadSize) + { + return HttpStatusCode.RequestEntityTooLarge; + } + + //Only ingest the rest of the message body if the request is not a head, get, or trace methods + if ((Request.Method & (HttpMethod.GET | HttpMethod.HEAD | HttpMethod.TRACE)) != 0) + { + //Bad format to include a message body with a GET, HEAD, or TRACE request + if (parseState.ContentLength > 0) + { + Config.ServerLog.Debug("Message body received from {ip} with GET, HEAD, or TRACE request, was considered an error and the request was dropped", Request.RemoteEndPoint); + return HttpStatusCode.BadRequest; + } + else + { + //Success! + return 0; + } + } + + //Check for chuncked transfer encoding + ReadOnlySpan<char> transfer = Request.Headers[HttpRequestHeader.TransferEncoding]; + if (!transfer.IsEmpty && transfer.Contains("chunked", StringComparison.OrdinalIgnoreCase)) + { + //Not a valid http version for chunked transfer encoding + if (Request.HttpVersion != HttpVersion.Http11) + { + return HttpStatusCode.BadRequest; + } + /* + * Was a content length also specified? + * This is an issue and is likely an attack. I am choosing not to support + * the HTTP 1.1 standard and will deny reading the rest of the data from the + * transport. + */ + if (parseState.ContentLength > 0) + { + Config.ServerLog.Debug("Possible attempted desync, Content length + chunked encoding specified. RemoteEP: {ip}", Request.RemoteEndPoint); + return HttpStatusCode.BadRequest; + } + + //Handle chunked transfer encoding (not implemented yet) + return HttpStatusCode.NotImplemented; + } + //Make sure non-zero cl header was provided + else if (parseState.ContentLength > 0) + { + //Open a temp buffer to store initial data in + ISlindingWindowBuffer<byte>? initData = reader.GetReminaingData(parseState.ContentLength); + //Setup the input stream and capture the initial data from the reader, and wrap the transport stream to read data directly + Request.InputStream.Prepare(parseState.ContentLength, initData); + Request.HasEntityBody = true; + } + //Success! + return 0; + } + } +} diff --git a/Net.Http/src/Core/Response/ChunkDataAccumulator.cs b/Net.Http/src/Core/Response/ChunkDataAccumulator.cs new file mode 100644 index 0000000..35c0275 --- /dev/null +++ b/Net.Http/src/Core/Response/ChunkDataAccumulator.cs @@ -0,0 +1,228 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: ChunkDataAccumulator.cs +* +* ChunkDataAccumulator.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Utils; +using VNLib.Utils.IO; + +using static VNLib.Net.Http.Core.CoreBufferHelpers; + +namespace VNLib.Net.Http.Core +{ + /// <summary> + /// A specialized <see cref="IDataAccumulator{T}"/> for buffering data + /// in Http/1.1 chunks + /// </summary> + internal class ChunkDataAccumulator : IDataAccumulator<byte>, IHttpLifeCycle + { + public const int RESERVED_CHUNK_SUGGESTION = 32; + + private readonly int BufferSize; + private readonly int ReservedSize; + private readonly Encoding Encoding; + private readonly ReadOnlyMemory<byte> CRLFBytes; + + public ChunkDataAccumulator(Encoding encoding, int bufferSize) + { + Encoding = encoding; + CRLFBytes = encoding.GetBytes(HttpHelpers.CRLF); + + ReservedSize = RESERVED_CHUNK_SUGGESTION; + BufferSize = bufferSize; + } + + private byte[]? _buffer; + private int _reservedOffset; + + + ///<inheritdoc/> + public int RemainingSize => _buffer!.Length - AccumulatedSize; + ///<inheritdoc/> + public Span<byte> Remaining => _buffer!.AsSpan(AccumulatedSize); + ///<inheritdoc/> + public Span<byte> Accumulated => _buffer!.AsSpan(_reservedOffset, AccumulatedSize); + ///<inheritdoc/> + public int AccumulatedSize { get; set; } + + private Memory<byte> CompleteChunk => _buffer.AsMemory(_reservedOffset, (AccumulatedSize - _reservedOffset)); + + /// <summary> + /// Attempts to buffer as much data as possible from the specified data + /// </summary> + /// <param name="data">The data to copy</param> + /// <returns>The number of bytes that were buffered</returns> + public ERRNO TryBufferChunk(ReadOnlySpan<byte> data) + { + //Calc data size and reserve space for final crlf + int dataToCopy = Math.Min(data.Length, RemainingSize - CRLFBytes.Length); + + //Write as much data as possible + data[..dataToCopy].CopyTo(Remaining); + //Advance buffer + Advance(dataToCopy); + + //Return number of bytes not written + return dataToCopy; + } + + ///<inheritdoc/> + public void Advance(int count) => AccumulatedSize += count; + + private void InitReserved() + { + //First reserve the chunk window by advancing the accumulator to the size + Advance(ReservedSize); + } + + ///<inheritdoc/> + public void Reset() + { + //zero offsets + _reservedOffset = 0; + AccumulatedSize = 0; + //Init reserved segment + InitReserved(); + } + + /// <summary> + /// Writes the buffered data as a single chunk to the stream asynchronously. The internal + /// state is reset if writing compleded successfully + /// </summary> + /// <param name="output">The stream to write data to</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>A value task that resolves when the data has been written to the stream</returns> + public async ValueTask FlushAsync(Stream output, CancellationToken cancellation) + { + //Update the chunk size + UpdateChunkSize(); + + //Write trailing chunk delimiter + this.Append(CRLFBytes.Span); + + //write to stream + await output.WriteAsync(CompleteChunk, cancellation); + + //Reset for next chunk + Reset(); + } + + /// <summary> + /// Writes the buffered data as a single chunk to the stream. The internal + /// state is reset if writing compleded successfully + /// </summary> + /// <param name="output">The stream to write data to</param> + /// <returns>A value task that resolves when the data has been written to the stream</returns> + public void Flush(Stream output) + { + //Update the chunk size + UpdateChunkSize(); + + //Write trailing chunk delimiter + this.Append(CRLFBytes.Span); + + //write to stream + output.Write(CompleteChunk.Span); + + //Reset for next chunk + Reset(); + } + + private void UpdateChunkSize() + { + const int CharBufSize = 2 * sizeof(int); + + /* + * Alloc stack buffer to store chunk size hex chars + * the size of the buffer should be at least the number + * of bytes of the max chunk size + */ + Span<char> s = stackalloc char[CharBufSize]; + + //Chunk size is the accumulated size without the reserved segment + int chunkSize = (AccumulatedSize - ReservedSize); + + //format the chunk size + chunkSize.TryFormat(s, out int written, "x"); + + //temp buffer to store encoded data in + Span<byte> encBuf = stackalloc byte[ReservedSize]; + //Encode the chunk size chars + int initOffset = Encoding.GetBytes(s[..written], encBuf); + + Span<byte> encoded = encBuf[..initOffset]; + + /* + * We need to calcuate how to store the encoded buffer directly + * before the accumulated chunk data. + * + * This requires us to properly upshift the reserved buffer to + * the exact size required to store the encoded chunk size + */ + + _reservedOffset = (ReservedSize - (initOffset + CRLFBytes.Length)); + + Span<byte> upshifted = _buffer!.AsSpan(_reservedOffset, ReservedSize); + + //First write the chunk size + encoded.CopyTo(upshifted); + + //Upshift again to write the crlf + upshifted = upshifted[initOffset..]; + + //Copy crlf + CRLFBytes.Span.CopyTo(upshifted); + } + + + public void OnNewRequest() + { + InitReserved(); + } + + public void OnComplete() + { + //Zero offsets + _reservedOffset = 0; + AccumulatedSize = 0; + } + + public void OnPrepare() + { + //Alloc buffer + _buffer = HttpBinBufferPool.Rent(BufferSize); + } + + public void OnRelease() + { + HttpBinBufferPool.Return(_buffer!); + _buffer = null; + } + + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/Response/ChunkedStream.cs b/Net.Http/src/Core/Response/ChunkedStream.cs new file mode 100644 index 0000000..7a8bebc --- /dev/null +++ b/Net.Http/src/Core/Response/ChunkedStream.cs @@ -0,0 +1,249 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: ChunkedStream.cs +* +* ChunkedStream.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +/* +* Provides a Chunked data-encoding stream for writing data-chunks to +* the transport using the basic chunked encoding format from MDN +* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding#directives +* +* This stream will buffer entire chunks to avoid multiple writes to the +* transport which can block or at minium cause overhead in context switching +* which should be mostly avoided but cause overhead in copying. Time profiling +* showed nearly equivalent performance for small chunks for synchronous writes. +* +*/ + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Utils; +using VNLib.Utils.Memory; + +namespace VNLib.Net.Http.Core +{ + + internal partial class HttpResponse + { + /// <summary> + /// Writes chunked HTTP message bodies to an underlying streamwriter + /// </summary> + private class ChunkedStream : Stream, IHttpLifeCycle + { + private const string LAST_CHUNK_STRING = "0\r\n\r\n"; + + private readonly ReadOnlyMemory<byte> LastChunk; + private readonly ChunkDataAccumulator ChunckAccumulator; + private readonly Func<Stream> GetTransport; + + private Stream? TransportStream; + private bool HadError; + + internal ChunkedStream(Encoding encoding, int chunkBufferSize, Func<Stream> getStream) + { + //Convert and store cached versions of the last chunk bytes + LastChunk = encoding.GetBytes(LAST_CHUNK_STRING); + + //get the min buffer by rounding to the nearest page + int actualBufSize = (chunkBufferSize / 4096 + 1) * 4096; + + //Init accumulator + ChunckAccumulator = new(encoding, actualBufSize); + + GetTransport = getStream; + } + + + 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 int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException("This stream cannot be read from"); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException("This stream does not support seeking"); + public override void SetLength(long value) => throw new NotSupportedException("This stream does not support seeking"); + public override void Flush() { } + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + + public override void Write(byte[] buffer, int offset, int count) => Write(new ReadOnlySpan<byte>(buffer, offset, count)); + public override void Write(ReadOnlySpan<byte> chunk) + { + //Only write non-zero chunks + if (chunk.Length <= 0) + { + return; + } + + //Init reader + ForwardOnlyReader<byte> reader = new(in chunk); + try + { + do + { + //try to accumulate the chunk data + ERRNO written = ChunckAccumulator.TryBufferChunk(reader.Window); + + //Not all data was buffered + if (written < reader.WindowSize) + { + //Advance reader + reader.Advance(written); + + //Flush accumulator + ChunckAccumulator.Flush(TransportStream!); + //Continue to buffer / flush as needed + continue; + } + break; + } + while (true); + } + catch + { + HadError = true; + throw; + } + } + + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + } + + public override async ValueTask WriteAsync(ReadOnlyMemory<byte> chunk, CancellationToken cancellationToken = default) + { + //Only write non-zero chunks + if (chunk.Length <= 0) + { + return; + } + + try + { + //Init reader + ForwardOnlyMemoryReader<byte> reader = new(in chunk); + + do + { + //try to accumulate the chunk data + ERRNO written = ChunckAccumulator.TryBufferChunk(reader.Window.Span); + + //Not all data was buffered + if (written < reader.WindowSize) + { + //Advance reader + reader.Advance(written); + + //Flush accumulator async + await ChunckAccumulator.FlushAsync(TransportStream!, cancellationToken); + //Continue to buffer / flush as needed + continue; + } + break; + } + while (true); + } + catch + { + HadError = true; + throw; + } + } + + public override async ValueTask DisposeAsync() + { + //If write error occured, then do not write the last chunk + if (HadError) + { + return; + } + + //Write remaining data to stream + await ChunckAccumulator.FlushAsync(TransportStream!, CancellationToken.None); + + //Write final chunk + await TransportStream!.WriteAsync(LastChunk, CancellationToken.None); + + //Flush base stream + await TransportStream!.FlushAsync(CancellationToken.None); + } + + protected override void Dispose(bool disposing) => Close(); + + public override void Close() + { + //If write error occured, then do not write the last chunk + if (HadError) + { + return; + } + + //Write remaining data to stream + ChunckAccumulator.Flush(TransportStream!); + + //Write final chunk + TransportStream!.Write(LastChunk.Span); + + //Flush base stream + TransportStream!.Flush(); + } + + + #region Hooks + + public void OnPrepare() + { + ChunckAccumulator.OnPrepare(); + } + + public void OnRelease() + { + ChunckAccumulator.OnRelease(); + } + + public void OnNewRequest() + { + ChunckAccumulator.OnNewRequest(); + + //Get transport stream even if not used + TransportStream = GetTransport(); + } + + public void OnComplete() + { + ChunckAccumulator.OnComplete(); + TransportStream = null; + + //Clear error flag + HadError = false; + } + + #endregion + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/Response/DirectStream.cs b/Net.Http/src/Core/Response/DirectStream.cs new file mode 100644 index 0000000..957c2a6 --- /dev/null +++ b/Net.Http/src/Core/Response/DirectStream.cs @@ -0,0 +1,96 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: DirectStream.cs +* +* DirectStream.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Net.Http.Core +{ + internal partial class HttpResponse + { + private class DirectStream : Stream + { + private Stream? BaseStream; + + public void Prepare(Stream transport) + { + BaseStream = transport; + } + + public override void Write(byte[] buffer, int offset, int count) + { + BaseStream!.Write(buffer, offset, count); + } + + public override void Write(ReadOnlySpan<byte> buffer) + { + BaseStream!.Write(buffer); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return BaseStream!.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) + { + return BaseStream!.WriteAsync(buffer, cancellationToken); + } + + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new InvalidOperationException("Stream does not have a length property"); + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override int Read(byte[] buffer, int offset, int count) => throw new InvalidOperationException("Stream does not support reading"); + public override long Seek(long offset, SeekOrigin origin) => throw new InvalidOperationException("Stream does not support seeking"); + public override void SetLength(long value) => throw new InvalidOperationException("Stream does not support seeking"); + + public override void Flush() => BaseStream!.Flush(); + public override Task FlushAsync(CancellationToken cancellationToken) => BaseStream!.FlushAsync(cancellationToken); + + + public override void Close() + { + BaseStream = null; + } + + + protected override void Dispose(bool disposing) + { + //Do not call base dispose + Close(); + } + + public override ValueTask DisposeAsync() + { + Close(); + return ValueTask.CompletedTask; + } + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/Response/HeaderDataAccumulator.cs b/Net.Http/src/Core/Response/HeaderDataAccumulator.cs new file mode 100644 index 0000000..715871f --- /dev/null +++ b/Net.Http/src/Core/Response/HeaderDataAccumulator.cs @@ -0,0 +1,157 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HeaderDataAccumulator.cs +* +* HeaderDataAccumulator.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Text; +using System.Runtime.InteropServices; + +using VNLib.Utils; +using VNLib.Utils.IO; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; +using static VNLib.Net.Http.Core.CoreBufferHelpers; + +namespace VNLib.Net.Http.Core +{ + internal partial class HttpResponse + { + + /// <summary> + /// Specialized data accumulator for compiling response headers + /// </summary> + private class HeaderDataAccumulator : IDataAccumulator<char>, IStringSerializeable, IHttpLifeCycle + { + private readonly int BufferSize; + + public HeaderDataAccumulator(int bufferSize) + { + //Calc correct char buffer size from bin buffer + this.BufferSize = bufferSize * sizeof(char); + } + + /* + * May be an issue but wanted to avoid alloc + * if possible since this is a field in a ref + * type + */ + + private UnsafeMemoryHandle<byte>? _handle; + + public void Advance(int count) + { + //Advance writer + AccumulatedSize += count; + } + + public void WriteLine() => this.Append(HttpHelpers.CRLF); + + public void WriteLine(ReadOnlySpan<char> data) + { + this.Append(data); + WriteLine(); + } + + /*Use bin buffers and cast to char buffer*/ + private Span<char> Buffer => MemoryMarshal.Cast<byte, char>(_handle!.Value.Span); + + public int RemainingSize => Buffer.Length - AccumulatedSize; + public Span<char> Remaining => Buffer[AccumulatedSize..]; + public Span<char> Accumulated => Buffer[..AccumulatedSize]; + public int AccumulatedSize { get; set; } + + /// <summary> + /// Encodes the buffered data and writes it to the stream, + /// attemts to avoid further allocation where possible + /// </summary> + /// <param name="enc"></param> + /// <param name="baseStream"></param> + public void Flush(Encoding enc, Stream baseStream) + { + ReadOnlySpan<char> span = Accumulated; + //Calc the size of the binary buffer + int byteSize = enc.GetByteCount(span); + //See if there is enough room in the current char buffer + if (RemainingSize < (byteSize / sizeof(char))) + { + //We need to alloc a binary buffer to write data to + using UnsafeMemoryHandle<byte> bin = GetBinBuffer(byteSize, false); + //encode data + int encoded = enc.GetBytes(span, bin.Span); + //Write to stream + baseStream.Write(bin.Span[..encoded]); + } + else + { + //Get bin buffer by casting remaining accumulator buffer + Span<byte> bin = MemoryMarshal.Cast<char, byte>(Remaining); + //encode data + int encoded = enc.GetBytes(span, bin); + //Write to stream + baseStream.Write(bin[..encoded]); + } + Reset(); + } + + public void Reset() => AccumulatedSize = 0; + + + + public void OnPrepare() + { + //Alloc buffer + _handle = GetBinBuffer(BufferSize, false); + } + + public void OnRelease() + { + _handle!.Value.Dispose(); + _handle = null; + } + + public void OnNewRequest() + {} + + public void OnComplete() + { + Reset(); + } + + + ///<inheritdoc/> + public string Compile() => Accumulated.ToString(); + ///<inheritdoc/> + public void Compile(ref ForwardOnlyWriter<char> writer) => writer.Append(Accumulated); + ///<inheritdoc/> + public ERRNO Compile(in Span<char> buffer) + { + ForwardOnlyWriter<char> writer = new(buffer); + Compile(ref writer); + return writer.Written; + } + ///<inheritdoc/> + public override string ToString() => Compile(); + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/Response/HttpContextExtensions.cs b/Net.Http/src/Core/Response/HttpContextExtensions.cs new file mode 100644 index 0000000..12702b3 --- /dev/null +++ b/Net.Http/src/Core/Response/HttpContextExtensions.cs @@ -0,0 +1,124 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpContextExtensions.cs +* +* HttpContextExtensions.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Net; +using System.Runtime.CompilerServices; + +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; + +namespace VNLib.Net.Http.Core +{ + /// <summary> + /// Provides extended funcionality of an <see cref="HttpContext"/> + /// </summary> + internal static class HttpContextExtensions + { + /// <summary> + /// Responds to a connection with the given status code + /// </summary> + /// <param name="ctx"></param> + /// <param name="code">The status code to send</param> + /// <exception cref="InvalidOperationException"></exception> + public static void Respond(this HttpContext ctx, HttpStatusCode code) => ctx.Response.SetStatusCode(code); + + /// <summary> + /// Begins a 301 redirection by sending status code and message heaaders to client. + /// </summary> + /// <param name="ctx"></param> + /// <param name="location">Location to direct client to, sets the "Location" header</param> + /// <exception cref="InvalidOperationException"></exception> + public static void Redirect301(this HttpContext ctx, Uri location) + { + ctx.Response.SetStatusCode(HttpStatusCode.MovedPermanently); + //Encode the string for propery http url formatting and set the location header + ctx.Response.Headers[HttpResponseHeader.Location] = location.ToString(); + } + + public const string NO_CACHE_STRING = "no-cache"; + private static readonly string CACHE_CONTROL_VALUE = HttpHelpers.GetCacheString(CacheType.NoCache | CacheType.NoStore); + + /// <summary> + /// Sets CacheControl and Pragma headers to no-cache + /// </summary> + /// <param name="Response"></param> + public static void SetNoCache(this HttpResponse Response) + { + Response.Headers[HttpResponseHeader.Pragma] = NO_CACHE_STRING; + Response.Headers[HttpResponseHeader.CacheControl] = CACHE_CONTROL_VALUE; + } + + /// <summary> + /// Sets the content-range header to the specified parameters + /// </summary> + /// <param name="Response"></param> + /// <param name="start">The content range start</param> + /// <param name="end">The content range end</param> + /// <param name="length">The total content length</param> + public static void SetContentRange(this HttpResponse Response, long start, long end, long length) + { + //Alloc enough space to hold the string + Span<char> buffer = stackalloc char[64]; + ForwardOnlyWriter<char> rangeBuilder = new(buffer); + //Build the range header in this format "bytes <begin>-<end>/<total>" + rangeBuilder.Append("bytes "); + rangeBuilder.Append(start); + rangeBuilder.Append('-'); + rangeBuilder.Append(end); + rangeBuilder.Append('/'); + rangeBuilder.Append(length); + //Print to a string and set the content range header + Response.Headers[HttpResponseHeader.ContentRange] = rangeBuilder.ToString(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ReadOnlyMemory<byte> GetRemainingConstrained(this IMemoryResponseReader reader, int limit) + { + //Calc the remaining bytes + int size = Math.Min(reader.Remaining, limit); + //get segment and slice + return reader.GetMemory()[..size]; + } + + /// <summary> + /// If an end-range is set, returns the remaining bytes up to the end-range, otherwise returns the entire request body length + /// </summary> + /// <param name="body"></param> + /// <param name="range">The data range</param> + /// <returns></returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static long GetResponseLengthWithRange(this IHttpResponseBody body, Tuple<long, long> range) + { + /* + * If end range is defined, then calculate the length of the response + * + * The length is the end range minus the start range plus 1 because range + * is an inclusve value + */ + + return range.Item2 < 0 ? body.Length : Math.Min(body.Length, range.Item2 - range.Item1 + 1); + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/Response/HttpContextResponseWriting.cs b/Net.Http/src/Core/Response/HttpContextResponseWriting.cs new file mode 100644 index 0000000..b03363e --- /dev/null +++ b/Net.Http/src/Core/Response/HttpContextResponseWriting.cs @@ -0,0 +1,253 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpContextResponseWriting.cs +* +* HttpContextResponseWriting.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Buffers; +using System.IO.Compression; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Net.Http.Core +{ + + internal partial class HttpContext + { + ///<inheritdoc/> + public async Task WriteResponseAsync(CancellationToken cancellation) + { + /* + * If exceptions are raised, the transport is unusable, the connection is terminated, + * and the release method will be called so the context can be reused + */ + + ValueTask discardTask = Request.InputStream.DiscardRemainingAsync(ParentServer.Config.DiscardBufferSize); + + //See if discard is needed + if (ResponseBody.HasData) + { + //Parallel the write and discard + Task response = WriteResponseInternalAsync(cancellation); + Task discard = discardTask.AsTask(); + + await Task.WhenAll(response, discard); + } + else + { + await discardTask; + } + + //Close response + await Response.CloseAsync(); + } + + /// <summary> + /// If implementing application set a response entity body, it is written to the output stream + /// </summary> + /// <param name="token">A token to cancel the operation</param> + private async Task WriteResponseInternalAsync(CancellationToken token) + { + //Adjust/append vary header + Response.Headers.Add(HttpResponseHeader.Vary, "Accept-Encoding"); + + //For head methods + if (Request.Method == HttpMethod.HEAD) + { + if (Request.Range != null) + { + //Get local range + Tuple<long, long> range = Request.Range; + + //Calc constrained content length + long length = ResponseBody.GetResponseLengthWithRange(range); + + //End range is inclusive so substract 1 + long endRange = (range.Item1 + length) - 1; + + //Set content-range header + Response.SetContentRange(range.Item1, endRange, length); + + //Specify what the content length would be + Response.Headers[HttpResponseHeader.ContentLength] = length.ToString(); + + } + else + { + //If the request method is head, do everything but send the body + Response.Headers[HttpResponseHeader.ContentLength] = ResponseBody.Length.ToString(); + } + + //We must send headers here so content length doesnt get overwritten + Response.FlushHeaders(); + } + else + { + Stream outputStream; + /* + * Process range header, data will not be compressed because that would + * require buffering, not a feature yet, and since the range will tell + * us the content length, we can get a direct stream to write to + */ + if (Request.Range != null) + { + //Get local range + Tuple<long, long> range = Request.Range; + + //Calc constrained content length + long length = ResponseBody.GetResponseLengthWithRange(range); + + //End range is inclusive so substract 1 + long endRange = (range.Item1 + length) - 1; + + //Set content-range header + Response.SetContentRange(range.Item1, endRange, length); + + //Get the raw output stream and set the length to the number of bytes + outputStream = Response.GetStream(length); + + await WriteEntityDataAsync(outputStream, length, token); + } + else + { + //Determine if compression should be used + bool compressionDisabled = + //disabled because app code disabled it + ContextFlags.IsSet(COMPRESSION_DISABLED_MSK) + //Disabled because too large or too small + || ResponseBody.Length >= ParentServer.Config.CompressionLimit + || ResponseBody.Length < ParentServer.Config.CompressionMinimum + //Disabled because lower than http11 does not support chunked encoding + || Request.HttpVersion < HttpVersion.Http11; + + //Get first compression method or none if disabled + HttpRequestExtensions.CompressionType ct = compressionDisabled ? HttpRequestExtensions.CompressionType.None : Request.GetCompressionSupport(); + + switch (ct) + { + case HttpRequestExtensions.CompressionType.Gzip: + { + //Specify gzip encoding (using chunked encoding) + Response.Headers[HttpResponseHeader.ContentEncoding] = "gzip"; + //get the chunked output stream + Stream chunked = Response.GetStream(); + //Use chunked encoding and send data as its written + outputStream = new GZipStream(chunked, ParentServer.Config.CompressionLevel, false); + } + break; + case HttpRequestExtensions.CompressionType.Deflate: + { + //Specify gzip encoding (using chunked encoding) + Response.Headers[HttpResponseHeader.ContentEncoding] = "deflate"; + //get the chunked output stream + Stream chunked = Response.GetStream(); + //Use chunked encoding and send data as its written + outputStream = new DeflateStream(chunked, ParentServer.Config.CompressionLevel, false); + } + break; + case HttpRequestExtensions.CompressionType.Brotli: + { + //Specify Brotli encoding (using chunked encoding) + Response.Headers[HttpResponseHeader.ContentEncoding] = "br"; + //get the chunked output stream + Stream chunked = Response.GetStream(); + //Use chunked encoding and send data as its written + outputStream = new BrotliStream(chunked, ParentServer.Config.CompressionLevel, false); + } + break; + //Default is no compression + case HttpRequestExtensions.CompressionType.None: + default: + //Since we know how long the response will be, we can submit it now (see note above for same issues) + outputStream = Response.GetStream(ResponseBody.Length); + break; + } + + //Write entity to output + await WriteEntityDataAsync(outputStream, token); + } + } + } + + private async Task WriteEntityDataAsync(Stream outputStream, CancellationToken token) + { + try + { + //Determine if buffer is required + if (ResponseBody.BufferRequired) + { + //Calc a buffer size (always a safe cast since rbs is an integer) + int bufferSize = (int)Math.Min((long)ParentServer.Config.ResponseBufferSize, ResponseBody.Length); + + //Alloc buffer, and dispose when completed + using IMemoryOwner<byte> buffer = CoreBufferHelpers.GetMemory(bufferSize, false); + + //Write response + await ResponseBody.WriteEntityAsync(outputStream, buffer.Memory, token); + } + //No buffer is required, write response directly + else + { + //Write without buffer + await ResponseBody.WriteEntityAsync(outputStream, null, token); + } + } + finally + { + //always dispose output stream + await outputStream.DisposeAsync(); + } + } + + private async Task WriteEntityDataAsync(Stream outputStream, long length, CancellationToken token) + { + try + { + //Determine if buffer is required + if (ResponseBody.BufferRequired) + { + //Calc a buffer size (always a safe cast since rbs is an integer) + int bufferSize = (int)Math.Min((long)ParentServer.Config.ResponseBufferSize, ResponseBody.Length); + + //Alloc buffer, and dispose when completed + using IMemoryOwner<byte> buffer = CoreBufferHelpers.GetMemory(bufferSize, false); + + //Write response + await ResponseBody.WriteEntityAsync(outputStream, length, buffer.Memory, token); + } + //No buffer is required, write response directly + else + { + //Write without buffer + await ResponseBody.WriteEntityAsync(outputStream, length, null, token); + } + } + finally + { + //always dispose output stream + await outputStream.DisposeAsync(); + } + } + } +} diff --git a/Net.Http/src/Core/Response/HttpResponse.cs b/Net.Http/src/Core/Response/HttpResponse.cs new file mode 100644 index 0000000..ab0971d --- /dev/null +++ b/Net.Http/src/Core/Response/HttpResponse.cs @@ -0,0 +1,307 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpResponse.cs +* +* HttpResponse.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +using VNLib.Utils.IO; + +namespace VNLib.Net.Http.Core +{ + internal partial class HttpResponse : IHttpLifeCycle + { + private readonly HashSet<HttpCookie> Cookies; + private readonly HeaderDataAccumulator Writer; + + private readonly DirectStream ReusableDirectStream; + private readonly ChunkedStream ReusableChunkedStream; + private readonly Func<Stream> _getStream; + private readonly Encoding ResponseEncoding; + private readonly Func<HttpVersion> GetVersion; + + private bool HeadersSent; + private bool HeadersBegun; + + private HttpStatusCode _code; + + /// <summary> + /// Response header collection + /// </summary> + public VnWebHeaderCollection Headers { get; } + + public HttpResponse(Encoding encoding, int headerBufferSize, int chunkedBufferSize, Func<Stream> getStream, Func<HttpVersion> getVersion) + { + //Initialize a new header collection and a cookie jar + Headers = new(); + Cookies = new(); + //Create a new reusable writer stream + Writer = new(headerBufferSize); + + _getStream = getStream; + ResponseEncoding = encoding; + + //Create a new chunked stream + ReusableChunkedStream = new(encoding, chunkedBufferSize, getStream); + ReusableDirectStream = new(); + GetVersion = getVersion; + } + + + /// <summary> + /// Sets the status code of the response + /// </summary> + /// <exception cref="InvalidOperationException"></exception> + internal void SetStatusCode(HttpStatusCode code) + { + if (HeadersBegun) + { + throw new InvalidOperationException("Status code has already been sent"); + } + + _code = code; + } + + /// <summary> + /// Adds a new http-cookie to the collection + /// </summary> + /// <param name="cookie">Cookie to add</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AddCookie(HttpCookie cookie) => Cookies.Add(cookie); + + /// <summary> + /// Allows sending an early 100-Continue status message to the client + /// </summary> + /// <exception cref="InvalidOperationException"></exception> + internal async Task SendEarly100ContinueAsync() + { + Check(); + //Send a status message with the continue response status + Writer.WriteLine(HttpHelpers.GetResponseString(GetVersion(), HttpStatusCode.Continue)); + //Trailing crlf + Writer.WriteLine(); + //get base stream + Stream bs = _getStream(); + //Flush writer to stream (will reset the buffer) + Writer.Flush(ResponseEncoding, bs); + //Flush the base stream + await bs.FlushAsync(); + } + + /// <summary> + /// Sends the status message and all available headers to the client. + /// Headers set after method returns will be sent when output stream is requested or scope exits + /// </summary> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="InvalidOperationException"></exception> + public void FlushHeaders() + { + Check(); + //If headers havent been sent yet, start with status code + if (!HeadersBegun) + { + //write status code first + Writer.WriteLine(HttpHelpers.GetResponseString(GetVersion(), _code)); + + //Write the date to header buffer + Writer.Append("Date: "); + Writer.Append(DateTimeOffset.UtcNow, "R"); + Writer.WriteLine(); + //Set begun flag + HeadersBegun = true; + } + //Write headers + for (int i = 0; i < Headers.Count; i++) + { + Writer.Append(Headers.Keys[i]); //Write header key + Writer.Append(": "); //Write separator + Writer.WriteLine(Headers[i]); //Write the header value + } + //Remove writen headers + Headers.Clear(); + //Write cookies if any are set + if (Cookies.Count > 0) + { + //Write cookies if any have been set + foreach (HttpCookie cookie in Cookies) + { + Writer.Append("Set-Cookie: "); + Writer.Append(in cookie); + Writer.WriteLine(); + } + //Clear all current cookies + Cookies.Clear(); + } + } + private void EndFlushHeaders(Stream transport) + { + //Sent all available headers + FlushHeaders(); + //Last line to end headers + Writer.WriteLine(); + + //Flush writer + Writer.Flush(ResponseEncoding, transport); + //Update sent headers + HeadersSent = true; + } + + /// <summary> + /// Gets a stream for writing data of a specified length directly to the client + /// </summary> + /// <param name="ContentLength"></param> + /// <returns>A <see cref="Stream"/> configured for writing data to client</returns> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="InvalidOperationException"></exception> + public Stream GetStream(long ContentLength) + { + Check(); + //Add content length header + Headers[HttpResponseHeader.ContentLength] = ContentLength.ToString(); + //End sending headers so the user can write to the ouput stream + Stream transport = _getStream(); + EndFlushHeaders(transport); + + //Init direct stream + ReusableDirectStream.Prepare(transport); + + //Return the direct stream + return ReusableDirectStream; + } + + /// <summary> + /// Sets up the client for chuncked encoding and gets a stream that allows for chuncks to be sent. User must call dispose on stream when done writing data + /// </summary> + /// <returns><see cref="Stream"/> supporting chunked encoding</returns> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="InvalidOperationException"></exception> + public Stream GetStream() + { +#if DEBUG + if (GetVersion() != HttpVersion.Http11) + { + throw new InvalidOperationException("Chunked transfer encoding is not acceptable for this http version"); + } +#endif + Check(); + //Set encoding type to chunked with user-defined compression + Headers[HttpResponseHeader.TransferEncoding] = "chunked"; + //End sending headers so the user can write to the ouput stream + Stream transport = _getStream(); + EndFlushHeaders(transport); + + //Return the reusable stream + return ReusableChunkedStream; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void Check() + { + if (HeadersSent) + { + throw new InvalidOperationException("Headers have already been sent!"); + } + } + + /// <summary> + /// Finalzies the response to a client by sending all available headers if + /// they have not been sent yet + /// </summary> + /// <exception cref="OutOfMemoryException"></exception> + internal async ValueTask CloseAsync() + { + //If headers havent been sent yet, send them and there must be no content + if (!HeadersBegun) + { + //RFC 7230, length only set on 200 + but not 204 + if ((int)_code >= 200 && (int)_code != 204) + { + //If headers havent been sent by this stage there is no content, so set length to 0 + Headers[HttpResponseHeader.ContentLength] = "0"; + } + //Flush transport + Stream transport = _getStream(); + EndFlushHeaders(transport); + //Flush transport + await transport.FlushAsync(); + } + //Headers have been started but not finished yet + else if (!HeadersSent) + { + //RFC 7230, length only set on 200 + but not 204 + if ((int)_code >= 200 && (int)_code != 204) + { + //If headers havent been sent by this stage there is no content, so set length to 0 + Headers[HttpResponseHeader.ContentLength] = "0"; + } + //If headers arent done sending yet, conclude headers + Stream transport = _getStream(); + EndFlushHeaders(transport); + //Flush transport + await transport.FlushAsync(); + } + } + + + public void OnPrepare() + { + //Propagate all child lifecycle hooks + Writer.OnPrepare(); + ReusableChunkedStream.OnPrepare(); + } + + public void OnRelease() + { + Writer.OnRelease(); + ReusableChunkedStream.OnRelease(); + } + + public void OnNewRequest() + { + //Default to okay status code + _code = HttpStatusCode.OK; + + Writer.OnNewRequest(); + ReusableChunkedStream.OnNewRequest(); + } + + public void OnComplete() + { + //Clear headers and cookies + Headers.Clear(); + Cookies.Clear(); + //Reset status values + HeadersBegun = false; + HeadersSent = false; + + //Call child lifecycle hooks + Writer.OnComplete(); + ReusableChunkedStream.OnComplete(); + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/Response/ResponseWriter.cs b/Net.Http/src/Core/Response/ResponseWriter.cs new file mode 100644 index 0000000..c9f20b5 --- /dev/null +++ b/Net.Http/src/Core/Response/ResponseWriter.cs @@ -0,0 +1,182 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: ResponseWriter.cs +* +* ResponseWriter.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Utils.Extensions; + + +namespace VNLib.Net.Http.Core +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "<Pending>")] + internal sealed class ResponseWriter : IHttpResponseBody, IHttpLifeCycle + { + private Stream? _streamResponse; + private IMemoryResponseReader? _memoryResponse; + + ///<inheritdoc/> + public bool HasData { get; private set; } + + //Buffering is required when a stream is set + bool IHttpResponseBody.BufferRequired => _streamResponse != null; + + ///<inheritdoc/> + public long Length { get; private set; } + + /// <summary> + /// Attempts to set the response body as a stream + /// </summary> + /// <param name="response">The stream response body to read</param> + /// <returns>True if the response entity could be set, false if it has already been set</returns> + internal bool TrySetResponseBody(Stream response) + { + if (HasData) + { + return false; + } + + //Get relative length of the stream, IE the remaning bytes in the stream if position has been modified + Length = (response.Length - response.Position); + //Store ref to stream + _streamResponse = response; + //update has-data flag + HasData = true; + return true; + } + + /// <summary> + /// Attempts to set the response entity + /// </summary> + /// <param name="response">The memory response to set</param> + /// <returns>True if the response entity could be set, false if it has already been set</returns> + internal bool TrySetResponseBody(IMemoryResponseReader response) + { + if (HasData) + { + return false; + } + + //Get length + Length = response.Remaining; + //Store ref to stream + _memoryResponse = response; + //update has-data flag + HasData = true; + return true; + } + + ///<inheritdoc/> + async Task IHttpResponseBody.WriteEntityAsync(Stream dest, long count, Memory<byte>? buffer, CancellationToken token) + { + //Write a sliding window response + if (_memoryResponse != null) + { + //Get min value from count/range length + int remaining = (int)Math.Min(count, _memoryResponse.Remaining); + + //Write response body from memory + while (remaining > 0) + { + //Get remaining segment + ReadOnlyMemory<byte> segment = _memoryResponse.GetRemainingConstrained(remaining); + + //Write segment to output stream + await dest.WriteAsync(segment, token); + + int written = segment.Length; + + //Advance by the written ammount + _memoryResponse.Advance(written); + + //Update remaining + remaining -= written; + } + } + else + { + //Buffer is required, and count must be supplied + await _streamResponse!.CopyToAsync(dest, buffer!.Value, count, token); + } + } + + ///<inheritdoc/> + async Task IHttpResponseBody.WriteEntityAsync(Stream dest, Memory<byte>? buffer, CancellationToken token) + { + //Write a sliding window response + if (_memoryResponse != null) + { + //Write response body from memory + while (_memoryResponse.Remaining > 0) + { + //Get segment + ReadOnlyMemory<byte> segment = _memoryResponse.GetMemory(); + + await dest.WriteAsync(segment, token); + + //Advance by + _memoryResponse.Advance(segment.Length); + } + } + else + { + //Buffer is required + await _streamResponse!.CopyToAsync(dest, buffer!.Value, token); + + //Try to dispose the response stream + await _streamResponse!.DisposeAsync(); + + //remove ref + _streamResponse = null; + } + } + + ///<inheritdoc/> + void IHttpLifeCycle.OnPrepare() + {} + + ///<inheritdoc/> + void IHttpLifeCycle.OnRelease() + {} + + ///<inheritdoc/> + void IHttpLifeCycle.OnNewRequest() + {} + + public void OnComplete() + { + //Clear has data flag + HasData = false; + Length = 0; + + //Clear rseponse containers + _streamResponse?.Dispose(); + _streamResponse = null; + _memoryResponse?.Close(); + _memoryResponse = null; + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/SharedHeaderReaderBuffer.cs b/Net.Http/src/Core/SharedHeaderReaderBuffer.cs new file mode 100644 index 0000000..36ebb66 --- /dev/null +++ b/Net.Http/src/Core/SharedHeaderReaderBuffer.cs @@ -0,0 +1,85 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: SharedHeaderReaderBuffer.cs +* +* SharedHeaderReaderBuffer.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Runtime.InteropServices; + +using VNLib.Utils.Memory; + + + +namespace VNLib.Net.Http.Core +{ + sealed class SharedHeaderReaderBuffer : IHttpLifeCycle + { + private UnsafeMemoryHandle<byte>? Handle; + + /// <summary> + /// The size of the binary buffer + /// </summary> + public int BinLength { get; } + + private readonly int _bufferSize; + + internal SharedHeaderReaderBuffer(int length) + { + _bufferSize = length + (length * sizeof(char)); + + //Bin buffer is the specified size + BinLength = length; + } + + /// <summary> + /// The binary buffer to store reader information + /// </summary> + public Span<byte> BinBuffer => Handle!.Value.Span[..BinLength]; + + /// <summary> + /// The char buffer to store read characters in + /// </summary> + public Span<char> CharBuffer => MemoryMarshal.Cast<byte, char>(Handle!.Value.Span[BinLength..]); + + public void OnPrepare() + { + //Alloc the shared buffer + Handle = CoreBufferHelpers.GetBinBuffer(_bufferSize, true); + } + + public void OnRelease() + { + //Free buffer + Handle?.Dispose(); + Handle = null; + } + + public void OnNewRequest() + {} + + public void OnComplete() + { + //Zero buffer + Handle!.Value.Span.Clear(); + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Core/VnHeaderCollection.cs b/Net.Http/src/Core/VnHeaderCollection.cs new file mode 100644 index 0000000..8ce3c88 --- /dev/null +++ b/Net.Http/src/Core/VnHeaderCollection.cs @@ -0,0 +1,75 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: VnHeaderCollection.cs +* +* VnHeaderCollection.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Net; +using System.Collections.Generic; + +namespace VNLib.Net.Http.Core +{ + internal sealed class VnHeaderCollection : IHeaderCollection + { + private VnWebHeaderCollection _RequestHeaders; + private VnWebHeaderCollection _ResponseHeaders; + + + IEnumerable<KeyValuePair<string, string>> IHeaderCollection.RequestHeaders => _RequestHeaders!; + + IEnumerable<KeyValuePair<string, string>> IHeaderCollection.ResponseHeaders => _ResponseHeaders!; + + internal VnHeaderCollection(HttpContext context) + { + _RequestHeaders = context.Request.Headers; + _ResponseHeaders = context.Response.Headers; + } + + string? IHeaderCollection.this[string index] + { + get => _RequestHeaders[index]; + set => _ResponseHeaders[index] = value; + } + + string IHeaderCollection.this[HttpResponseHeader index] + { + set => _ResponseHeaders[index] = value; + } + + string? IHeaderCollection.this[HttpRequestHeader index] => _RequestHeaders[index]; + + bool IHeaderCollection.HeaderSet(HttpResponseHeader header) => !string.IsNullOrEmpty(_ResponseHeaders[header]); + + bool IHeaderCollection.HeaderSet(HttpRequestHeader header) => !string.IsNullOrEmpty(_RequestHeaders[header]); + + void IHeaderCollection.Append(HttpResponseHeader header, string? value) => _ResponseHeaders.Add(header, value); + + void IHeaderCollection.Append(string header, string? value) => _ResponseHeaders.Add(header, value); + +#nullable disable + internal void Clear() + { + _RequestHeaders = null; + _ResponseHeaders = null; + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Exceptions/ContentTypeException.cs b/Net.Http/src/Exceptions/ContentTypeException.cs new file mode 100644 index 0000000..abff151 --- /dev/null +++ b/Net.Http/src/Exceptions/ContentTypeException.cs @@ -0,0 +1,43 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: ContentTypeException.cs +* +* ContentTypeException.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Net.Http +{ + /// <summary> + /// Thrown when the application attempts to submit a response to a client + /// when the client does not accept the given content type + /// </summary> + public sealed class ContentTypeUnacceptableException:FormatException + { + public ContentTypeUnacceptableException(string message) : base(message) {} + + public ContentTypeUnacceptableException() + {} + + public ContentTypeUnacceptableException(string message, Exception innerException) : base(message, innerException) + {} + } +}
\ No newline at end of file diff --git a/Net.Http/src/Exceptions/TerminateConnectionException.cs b/Net.Http/src/Exceptions/TerminateConnectionException.cs new file mode 100644 index 0000000..b854b6e --- /dev/null +++ b/Net.Http/src/Exceptions/TerminateConnectionException.cs @@ -0,0 +1,57 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: TerminateConnectionException.cs +* +* TerminateConnectionException.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Net; + +namespace VNLib.Net.Http +{ + /// <summary> + /// User code may throw this exception to signal the <see cref="HttpServer"/> to drop + /// the transport connection and return an optional status code + /// </summary> + public class TerminateConnectionException : Exception + { + internal HttpStatusCode Code { get; } + + /// <summary> + /// Creates a new instance that terminates the connection without sending a response to the connection + /// </summary> + public TerminateConnectionException() : base(){} + /// <summary> + /// Creates a new instance of the connection exception with an error code to respond to the connection with + /// </summary> + /// <param name="responseCode">The status code to send to the user</param> + public TerminateConnectionException(HttpStatusCode responseCode) + { + this.Code = responseCode; + } + + public TerminateConnectionException(string message) : base(message) + {} + + public TerminateConnectionException(string message, Exception innerException) : base(message, innerException) + {} + } +}
\ No newline at end of file diff --git a/Net.Http/src/FileUpload.cs b/Net.Http/src/FileUpload.cs new file mode 100644 index 0000000..654d682 --- /dev/null +++ b/Net.Http/src/FileUpload.cs @@ -0,0 +1,122 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: FileUpload.cs +* +* FileUpload.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Text; + +using VNLib.Utils.IO; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; + +using static VNLib.Net.Http.Core.CoreBufferHelpers; + +namespace VNLib.Net.Http +{ + /// <summary> + /// Represents an file that was received as an entity body, either using Multipart/FormData or as the entity body itself + /// </summary> + public readonly struct FileUpload + { + /// <summary> + /// Content type of uploaded file + /// </summary> + public readonly ContentType ContentType; + /// <summary> + /// Name of file uploaded + /// </summary> + public readonly string FileName; + /// <summary> + /// The file data captured on upload + /// </summary> + public readonly Stream FileData; + + private readonly bool OwnsHandle; + + /// <summary> + /// Allocates a new binary buffer, encodes, and copies the specified data to a new <see cref="FileUpload"/> + /// structure of the specified content type + /// </summary> + /// <param name="data">The string data to copy</param> + /// <param name="dataEncoding">The encoding instance to encode the string data from</param> + /// <param name="filename">The name of the file</param> + /// <param name="ct">The content type of the file data</param> + /// <returns>The <see cref="FileUpload"/> container</returns> + internal static FileUpload FromString(ReadOnlySpan<char> data, Encoding dataEncoding, string filename, ContentType ct) + { + //get number of bytes + int bytes = dataEncoding.GetByteCount(data); + //get a buffer from the HTTP heap + MemoryHandle<byte> buffHandle = HttpPrivateHeap.Alloc<byte>(bytes); + try + { + //Convert back to binary + bytes = dataEncoding.GetBytes(data, buffHandle); + + //Create a new memory stream encapsulating the file data + VnMemoryStream vms = VnMemoryStream.ConsumeHandle(buffHandle, bytes, true); + + //Create new upload wrapper + return new (vms, filename, ct, true); + } + catch + { + //Make sure the hanle gets disposed if there is an error + buffHandle.Dispose(); + throw; + } + } + + /// <summary> + /// Initialzes a new <see cref="FileUpload"/> structure from the specified data + /// and file information. + /// </summary> + /// <param name="data"></param> + /// <param name="filename"></param> + /// <param name="ct"></param> + /// <param name="ownsHandle"></param> + public FileUpload(Stream data, string filename, ContentType ct, bool ownsHandle) + { + FileName = filename; + ContentType = ct; + //Store handle ownership + OwnsHandle = ownsHandle; + //Store the stream + FileData = data; + } + + /// <summary> + /// Releases any memory the current instance holds if it owns the handles + /// </summary> + internal readonly void Free() + { + //Dispose the handle if we own it + if (OwnsHandle) + { + //This should always be synchronous + FileData.Dispose(); + } + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Helpers/AlternateProtocolTransportStreamWrapper.cs b/Net.Http/src/Helpers/AlternateProtocolTransportStreamWrapper.cs new file mode 100644 index 0000000..88891fb --- /dev/null +++ b/Net.Http/src/Helpers/AlternateProtocolTransportStreamWrapper.cs @@ -0,0 +1,50 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: AlternateProtocolTransportStreamWrapper.cs +* +* AlternateProtocolTransportStreamWrapper.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Threading.Tasks; + +using VNLib.Utils.IO; + +namespace VNLib.Net.Http.Core +{ + internal class AlternateProtocolTransportStreamWrapper : BackingStream<Stream> + { + public AlternateProtocolTransportStreamWrapper(Stream transport) + { + this.BaseStream = transport; + } + + //Do not allow the caller to dispose the transport stream + protected override void Dispose(bool disposing) + {} + public override ValueTask DisposeAsync() + { + return ValueTask.CompletedTask; + } + public override void Close() + {} + } +} diff --git a/Net.Http/src/Helpers/ContentType.cs b/Net.Http/src/Helpers/ContentType.cs new file mode 100644 index 0000000..ce7b7ce --- /dev/null +++ b/Net.Http/src/Helpers/ContentType.cs @@ -0,0 +1,1180 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: ContentType.cs +* +* ContentType.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +namespace VNLib.Net.Http +{ + /// <summary> + /// Mime content type + /// </summary> + public enum ContentType + { + NonSupported, + MultiPart, + UrlEncoded, + Aab, + Aac, + Aam, + Aas, + Abw, + Ac, + Acc, + Ace, + Acu, + Acutc, + Adp, + Aep, + Afm, + Afp, + Ahead, + Ai, + Aif, + Aifc, + Aiff, + Air, + Ait, + Ami, + Amr, + Apk, + Apng, + Appcache, + Apr, + Arc, + Arj, + Asc, + Asf, + Asm, + Aso, + Asx, + Atc, + Atom, + Atomcat, + Atomsvc, + Atx, + Au, + Avi, + Avif, + Aw, + Azf, + Azs, + Azv, + Azw, + B16, + Bat, + Bcpio, + Bdf, + Bdm, + Bdoc, + Bed, + Bh2, + Binary, + Blb, + Blorb, + Bmi, + Bmml, + Bmp, + Book, + Box, + Boz, + Bpk, + Bsp, + Btif, + Buffer, + Bz, + Bz2, + C, + C11amc, + C11amz, + C4d, + C4f, + C4g, + C4p, + C4u, + Cab, + Caf, + Cap, + Car, + Cat, + Cb7, + Cba, + Cbr, + Cbt, + Cbz, + Cc, + Cco, + Cct, + Ccxml, + Cdbcmsg, + Cdf, + Cdfx, + Cdkey, + Cdmia, + Cdmic, + Cdmid, + Cdmio, + Cdmiq, + Cdx, + Cdxml, + Cdy, + Cer, + Cfs, + Cgm, + Chat, + Chm, + Chrt, + Cif, + Cii, + Cil, + Cjs, + Cla, + Clkk, + Clkp, + Clkt, + Clkw, + Clkx, + Clp, + Cmc, + Cmdf, + Cml, + Cmp, + Cmx, + Cod, + Coffee, + Com, + Conf, + Cpio, + Cpp, + Cpt, + Crd, + Crl, + Crt, + Crx, + Csh, + Csl, + Csml, + Csp, + Css, + Cst, + Csv, + Cu, + Curl, + Cww, + Cxt, + Cxx, + Dae, + Daf, + Dart, + Dataless, + Davmount, + Dbf, + Dbk, + Dcr, + Dcurl, + Dd2, + Ddd, + Ddf, + Dds, + Deb, + Def, + Deploy, + Der, + Dfac, + Dgc, + Dic, + Dir, + Dis, + Dist, + Distz, + Djv, + Djvu, + Dll, + Dmg, + Dmp, + Dms, + Dna, + Doc, + Docm, + Docx, + Dot, + Dotm, + Dotx, + Dp, + Dpg, + Dra, + Drle, + Dsc, + Dssc, + Dtb, + Dtd, + Dts, + Dtshd, + Dump, + Dvb, + Dvi, + Dwd, + Dwf, + Dwg, + Dxf, + Dxp, + Dxr, + Ear, + Ecma, + Edm, + Edx, + Efif, + Ei6, + Elc, + Emf, + Eml, + Emma, + Emz, + Eol, + Eot, + Eps, + Epub, + Es, + Es3, + Esa, + Esf, + Et3, + Etx, + Eva, + Evy, + Exe, + Exi, + Exp, + Exr, + Ext, + Ez, + Ez2, + Ez3, + F, + F4v, + Fortran, + F90, + Fbs, + Fcdt, + Fcs, + Fdf, + Fdt, + Fg5, + Fgd, + Fh, + Fh4, + Fh5, + Fh7, + Fhc, + Fig, + Fits, + Flac, + Fli, + Flo, + Flv, + Flw, + Flx, + Fly, + Fm, + Fnc, + Fo, + For, + Fpx, + Frame, + Fsc, + Fst, + Ftc, + Fti, + Fvt, + Fxp, + Fxpl, + Fzs, + G2w, + G3, + G3w, + Gac, + Gam, + Gbr, + Gca, + Gdl, + Gdoc, + Geo, + Geojson, + Gex, + Ggb, + Ggt, + Ghf, + Gif, + Gim, + Glb, + Gltf, + Gml, + Gmx, + Gnumeric, + Gph, + Gpx, + Gqf, + Gqs, + Gram, + Gramps, + Gre, + Grv, + Grxml, + Gsf, + Gsheet, + Gslides, + Gtar, + Gtm, + Gtw, + Gv, + Gxf, + Gxt, + Gz, + H, + H261, + H263, + H264, + Hal, + Hbci, + Hbs, + Hdd, + Hdf, + Heic, + Heics, + Heif, + Heifs, + Hej2, + Held, + Hh, + Hjson, + Hlp, + Hpgl, + Hpid, + Hps, + Hqx, + Hsj2, + Htc, + Htke, + Htm, + Html, + Hvd, + Hvp, + Hvs, + I2g, + Icc, + Ice, + Icm, + Ico, + Ics, + Ief, + Ifb, + Ifm, + Iges, + Igl, + Igm, + Igs, + Igx, + Iif, + Img, + Imp, + Ims, + Ini, + Ink, + Inkml, + Install, + Iota, + Ipfix, + Ipk, + Irm, + Irp, + Iso, + Itp, + Its, + Ivp, + Ivu, + Jad, + Jade, + Jam, + Jar, + Jardiff, + Java, + Jhc, + Jisp, + Jls, + Jlt, + Jng, + Jnlp, + Joda, + Jp2, + Jpe, + Jpeg, + Jpf, + Jpg, + Jpg2, + Jpgm, + Jpgv, + Jph, + Jpm, + Jpx, + Javascript, + Json, + Json5, + Jsonld, + Jsonml, + Jsx, + Jxr, + Jxra, + Jxrs, + Jxs, + Jxsc, + Jxsi, + Jxss, + Kar, + Karbon, + Kdbx, + Key, + Kfo, + Kia, + Kml, + Kmz, + Kne, + Knp, + Kon, + Kpr, + Kpt, + Kpxx, + Ksp, + Ktr, + Ktx, + Ktx2, + Ktz, + Kwd, + Kwt, + Lasxml, + Latex, + Lbd, + Lbe, + Les, + Less, + Lgr, + Lha, + Link66, + List, + List3820, + Listafp, + Lnk, + Log, + Lostxml, + Lrf, + Lrm, + Ltf, + Lua, + Luac, + Lvp, + Lwp, + Lzh, + M13, + M14, + M1v, + M21, + M2a, + M2v, + M3a, + M3u, + M3u8, + M4a, + M4p, + M4s, + M4u, + M4v, + Ma, + Mads, + Maei, + Mag, + Maker, + Man, + Manifest, + Map, + Mar, + Markdown, + Mathml, + Mb, + Mbk, + Mbox, + Mc1, + Mcd, + Mcurl, + Md, + Mdb, + Mdi, + Mdx, + Me, + Mesh, + Meta4, + Metalink, + Mets, + Mfm, + Mft, + Mgp, + Mgz, + Mid, + Midi, + Mie, + Mif, + Mime, + Mj2, + Mjp2, + Mjs, + Mk3d, + Mka, + Mkd, + Mks, + Mkv, + Mlp, + Mmd, + Mmf, + Mml, + Mmr, + Mng, + Mny, + Mobi, + Mods, + Mov, + Movie, + Mp2, + Mp21, + Mp2a, + Mp3, + Mp4, + Mp4a, + Mp4s, + Mp4v, + Mpc, + Mpd, + Mpe, + Mpeg, + Mpg, + Mpg4, + Mpga, + Mpkg, + Mpm, + Mpn, + Mpp, + Mpt, + Mpy, + Mqy, + Mrc, + Mrcx, + Ms, + Mscml, + Mseed, + Mseq, + Msf, + Msg, + Msh, + Msi, + Msl, + Msm, + Msp, + Msty, + Mtl, + Mts, + Mus, + Musd, + Musicxml, + Mvb, + Mvt, + Mwf, + Mxf, + Mxl, + Mxmf, + Mxml, + Mxs, + Mxu, + N3, + Nb, + Nbp, + Nc, + Ncx, + Nfo, + Ngdat, + Nitf, + Nlu, + Nml, + Nnd, + Nns, + Nnw, + Npx, + Nq, + Nsc, + Nsf, + Nt, + Ntf, + Numbers, + Nzb, + Oa2, + Oa3, + Oas, + Obd, + Obgx, + Obj, + Oda, + Odb, + Odc, + Odf, + Odft, + Odg, + Odi, + Odm, + Odp, + Ods, + Odt, + Oga, + Ogex, + Ogg, + Ogv, + Ogx, + Omdoc, + Onepkg, + Onetmp, + Onetoc, + Onetoc2, + Opf, + Opml, + Oprc, + Opus, + Org, + Osf, + Osfpvg, + Osm, + Otc, + Otf, + Otg, + Oth, + Oti, + Otp, + Ots, + Ott, + Ova, + Ovf, + Owl, + Oxps, + Oxt, + P, + P10, + P12, + P7b, + P7c, + P7m, + P7r, + P7s, + P8, + Pac, + Pages, + Pas, + Paw, + Pbd, + Pbm, + Pcap, + Pcf, + Pcl, + Pclxl, + Pct, + Pcurl, + Pcx, + Pdb, + Pde, + Pdf, + Pem, + Pfa, + Pfb, + Pfm, + Pfr, + Pfx, + Pgm, + Pgn, + Pgp, + Php, + Pic, + Pkg, + Pki, + Pkipath, + Pkpass, + Pl, + Plb, + Plc, + Plf, + Pls, + Pm, + Pml, + Png, + Pnm, + Portpkg, + Pot, + Potm, + Potx, + Ppam, + Ppd, + Ppm, + Pps, + Ppsm, + Ppsx, + Ppt, + Pptm, + Pptx, + Pqa, + Prc, + Pre, + Prf, + Provx, + Ps, + Psb, + Psd, + Psf, + Pskcxml, + Pti, + Ptid, + Pub, + Pvb, + Pwn, + Pya, + Pyv, + Qam, + Qbo, + Qfx, + Qps, + Qt, + Qwd, + Qwt, + Qxb, + Qxd, + Qxl, + Qxt, + Ra, + Ram, + Raml, + Rapd, + Rar, + Ras, + Rdf, + Rdz, + Relo, + Rep, + Res, + Rgb, + Rif, + Rip, + Ris, + Rl, + Rlc, + Rld, + Rm, + Rmi, + Rmp, + Rms, + Rmvb, + Rnc, + Rng, + Roa, + Roff, + Rp9, + Rpm, + Rpss, + Rpst, + Rq, + Rs, + Rsat, + Rsd, + Rsheet, + Rss, + Rtf, + Rtx, + Run, + Rusd, + S, + S3m, + Saf, + Sass, + Sbml, + Sc, + Scd, + Scm, + Scq, + Scs, + Scss, + Scurl, + Sda, + Sdc, + Sdd, + Sdkd, + Sdkm, + Sdp, + Sdw, + Sea, + See, + Seed, + Sema, + Semd, + Semf, + Senmlx, + Sensmlx, + Ser, + Setpay, + Setreg, + Sfs, + Sfv, + Sgi, + Sgl, + Sgm, + Sgml, + Sh, + Shar, + Shex, + Shf, + Shtml, + Sid, + Sieve, + Sig, + Sil, + Silo, + Sis, + Sisx, + Sit, + Sitx, + Siv, + Skd, + Skm, + Skp, + Skt, + Sldm, + Sldx, + Slim, + Slm, + Sls, + Slt, + Sm, + Smf, + Smi, + Smil, + Smv, + Smzip, + Snd, + Snf, + So, + Spc, + Spdx, + Spf, + Spl, + Spot, + Spp, + Spq, + Spx, + Sql, + Src, + Srt, + Sru, + Srx, + Ssdl, + Sse, + Ssf, + Ssml, + St, + Stc, + Std, + Stf, + Sti, + Stk, + Stl, + Stpx, + Stpxz, + Stpz, + Str, + Stw, + Styl, + Stylus, + Sub, + Sus, + Susp, + Sv4cpio, + Sv4crc, + Svc, + Svd, + Svg, + Svgz, + Swa, + Swf, + Swi, + Swidtag, + Sxc, + Sxd, + Sxg, + Sxi, + Sxm, + Sxw, + T, + T3, + T38, + Taglet, + Tao, + Tap, + Tar, + Tcap, + Tcl, + Td, + Teacher, + Tei, + Tex, + Texi, + Texinfo, + Text, + Tfi, + Tfm, + Tfx, + Tga, + Thmx, + Tif, + Tiff, + Tk, + Tmo, + Toml, + Torrent, + Tpl, + Tpt, + Tr, + Tra, + Trig, + Trm, + Ts, + Tsd, + Tsv, + Ttc, + Ttf, + Ttl, + Ttml, + Twd, + Twds, + Txd, + Txf, + Txt, + U32, + U8dsn, + U8hdr, + U8mdn, + U8msg, + Ubj, + Udeb, + Ufd, + Ufdl, + Ulx, + Umj, + Unityweb, + Uoml, + Uri, + Uris, + Urls, + Usdz, + Ustar, + Utz, + Uu, + Uva, + Uvd, + Uvf, + Uvg, + Uvh, + Uvi, + Uvm, + Uvp, + Uvs, + Uvt, + Uvu, + Uvv, + Uvva, + Uvvd, + Uvvf, + Uvvg, + Uvvh, + Uvvi, + Uvvm, + Uvvp, + Uvvs, + Uvvt, + Uvvu, + Uvvv, + Uvvx, + Uvvz, + Uvx, + Uvz, + Vbox, + Vcard, + Vcd, + Vcf, + Vcg, + Vcs, + Vcx, + Vdi, + Vds, + Vhd, + Vis, + Viv, + Vmdk, + Vob, + Vor, + Vox, + Vrml, + Vsd, + Vsf, + Vss, + Vst, + Vsw, + Vtf, + Vtt, + Vtu, + Vxml, + W3d, + Wad, + Wadl, + War, + Wasm, + Wav, + Wax, + Wbmp, + Wbs, + Wbxml, + Wcm, + Wdb, + Wdp, + Weba, + Webapp, + Webm, + Webp, + Wg, + Wgt, + Wks, + Wm, + Wma, + Wmd, + Wmf, + Wml, + Wmlc, + Wmls, + Wmlsc, + Wmv, + Wmx, + Wmz, + Woff, + Woff2, + Wpd, + Wpl, + Wps, + Wqd, + Wri, + Wrl, + Wsc, + Wsdl, + Wspolicy, + Wtb, + Wvx, + X32, + X3d, + X3db, + X3dbz, + X3dv, + X3dvz, + X3dz, + Xaml, + Xap, + Xar, + Xav, + Xbap, + Xbd, + Xbm, + Xca, + Xcs, + Xdf, + Xdm, + Xdp, + Xdssc, + Xdw, + Xel, + Xenc, + Xer, + Xfdf, + Xfdl, + Xht, + Xhtml, + Xhvml, + Xif, + Xla, + Xlam, + Xlc, + Xlf, + Xlm, + Xls, + Xlsb, + Xlsm, + Xlsx, + Xlt, + Xltm, + Xltx, + Xlw, + Xm, + Xml, + Xns, + Xo, + Xop, + Xpi, + Xpl, + Xpm, + Xpr, + Xps, + Xpw, + Xpx, + Xsd, + Xsl, + Xslt, + Xsm, + Xspf, + Xul, + Xvm, + Xvml, + Xwd, + Xyz, + Xz, + Yaml, + Yang, + Yin, + Yml, + Ymp, + Z1, + Z2, + Z3, + Z4, + Z5, + Z6, + Z7, + Z8, + Zaz, + Zip, + Zir, + Zirz, + Zmm, + } +}
\ No newline at end of file diff --git a/Net.Http/src/Helpers/CoreBufferHelpers.cs b/Net.Http/src/Helpers/CoreBufferHelpers.cs new file mode 100644 index 0000000..5c5d918 --- /dev/null +++ b/Net.Http/src/Helpers/CoreBufferHelpers.cs @@ -0,0 +1,188 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: CoreBufferHelpers.cs +* +* CoreBufferHelpers.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +/* + * This class is meant to provide memory helper methods + * as a centralized HTTP local memory api. + * + * Pools and heaps are privatized to help avoid + * leaking sensitive HTTP data across other application + * allocations and help provide memory optimization. + */ + + + +using System; +using System.IO; +using System.Buffers; +using System.Security; +using System.Threading; + +using VNLib.Utils.IO; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; + +namespace VNLib.Net.Http.Core +{ + + /// <summary> + /// Provides memory pools and an internal heap for allocations. + /// </summary> + internal static class CoreBufferHelpers + { + private class InitDataBuffer : ISlindingWindowBuffer<byte> + { + private readonly ArrayPool<byte> pool; + private readonly int size; + + private byte[]? buffer; + + public InitDataBuffer(ArrayPool<byte> pool, int size) + { + this.buffer = pool.Rent(size, true); + this.pool = pool; + this.size = size; + WindowStartPos = 0; + WindowEndPos = 0; + } + + public int WindowStartPos { get; set; } + public int WindowEndPos { get; set; } + Memory<byte> ISlindingWindowBuffer<byte>.Buffer => buffer.AsMemory(0, size); + + public void Advance(int count) + { + WindowEndPos += count; + } + + public void AdvanceStart(int count) + { + WindowStartPos += count; + } + + public void Reset() + { + WindowStartPos = 0; + WindowEndPos = 0; + } + + //Release the buffer back to the pool + void ISlindingWindowBuffer<byte>.Close() + { + pool.Return(buffer!); + buffer = null; + } + } + + /// <summary> + /// An internal HTTP character binary pool for HTTP specific internal buffers + /// </summary> + public static ArrayPool<byte> HttpBinBufferPool { get; } = ArrayPool<byte>.Create(); + /// <summary> + /// An <see cref="IUnmangedHeap"/> used for internal HTTP buffers + /// </summary> + public static IUnmangedHeap HttpPrivateHeap => _lazyHeap.Value; + + private static readonly Lazy<IUnmangedHeap> _lazyHeap = new(Memory.InitializeNewHeapForProcess, LazyThreadSafetyMode.PublicationOnly); + + /// <summary> + /// Alloctes an unsafe block of memory from the internal heap, or buffer pool + /// </summary> + /// <param name="size">The number of elemnts to allocate</param> + /// <param name="zero">A value indicating of the block should be zeroed before returning</param> + /// <returns>A handle to the block of memory</returns> + /// <exception cref="SecurityException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + public static UnsafeMemoryHandle<byte> GetBinBuffer(int size, bool zero) + { + //Calc buffer size to the nearest page size + size = (size / 4096 + 1) * 4096; + + //If rpmalloc lib is loaded, use it + if (Memory.IsRpMallocLoaded) + { + return Memory.Shared.UnsafeAlloc<byte>(size, zero); + } + else if (size > Memory.MAX_UNSAFE_POOL_SIZE) + { + return HttpPrivateHeap.UnsafeAlloc<byte>(size, zero); + } + else + { + return new(HttpBinBufferPool, size, zero); + } + } + + public static IMemoryOwner<byte> GetMemory(int size, bool zero) + { + //Calc buffer size to the nearest page size + size = (size / 4096 + 1) * 4096; + + //If rpmalloc lib is loaded, use it + if (Memory.IsRpMallocLoaded) + { + return Memory.Shared.DirectAlloc<byte>(size, zero); + } + //Avoid locking in heap unless the buffer is too large to alloc array + else if (size > Memory.MAX_UNSAFE_POOL_SIZE) + { + return HttpPrivateHeap.DirectAlloc<byte>(size, zero); + } + else + { + //Convert temp buffer to memory owner + +#pragma warning disable CA2000 // Dispose objects before losing scope + return new VnTempBuffer<byte>(HttpBinBufferPool, size, zero).ToMemoryManager(); +#pragma warning restore CA2000 // Dispose objects before losing scope + } + } + + /// <summary> + /// Gets the remaining data in the reader buffer and prepares a + /// sliding window buffer to read data from + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="reader"></param> + /// <param name="maxContentLength">Maximum content size to clamp the remaining buffer window to</param> + /// <returns></returns> + public static ISlindingWindowBuffer<byte>? GetReminaingData<T>(this ref T reader, long maxContentLength) where T: struct, IVnTextReader + { + //clamp max available to max content length + int available = Math.Clamp(reader.Available, 0, (int)maxContentLength); + if (available <= 0) + { + return null; + } + //Alloc sliding window buffer + ISlindingWindowBuffer<byte> buffer = new InitDataBuffer(HttpBinBufferPool, available); + //Read remaining data + reader.ReadRemaining(buffer.RemainingBuffer.Span); + //Advance the buffer to the end of available data + buffer.Advance(available); + return buffer; + } + + } +} diff --git a/Net.Http/src/Helpers/HelperTypes.cs b/Net.Http/src/Helpers/HelperTypes.cs new file mode 100644 index 0000000..e89ca78 --- /dev/null +++ b/Net.Http/src/Helpers/HelperTypes.cs @@ -0,0 +1,98 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HelperTypes.cs +* +* HelperTypes.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Net.Http +{ + [Flags] + public enum HttpMethod + { + NOT_SUPPORTED, + GET = 0x01, + POST = 0x02, + PUT = 0x04, + OPTIONS = 0x08, + HEAD = 0x10, + MERGE = 0x20, + COPY = 0x40, + DELETE = 0x80, + PATCH = 0x100, + TRACE = 0x200, + MOVE = 0x400, + LOCK = 0x800 + } + /// <summary> + /// HTTP protocol version + /// </summary> + [Flags] + public enum HttpVersion + { + NotSupported, + Http1 = 0x01, + Http11 = 0x02, + Http2 = 0x04, + Http09 = 0x08 + } + /// <summary> + /// HTTP response entity cache flags + /// </summary> + [Flags] + public enum CacheType + { + Ignore = 0x00, + NoCache = 0x01, + Private = 0x02, + Public = 0x04, + NoStore = 0x08, + Revalidate = 0x10 + } + + /// <summary> + /// Specifies an HTTP cookie SameSite type + /// </summary> + public enum CookieSameSite + { + Lax, None, SameSite + } + + /// <summary> + /// Low level 301 "hard" redirect + /// </summary> + public class Redirect + { + public readonly string Url; + public readonly Uri RedirectUrl; + /// <summary> + /// Quickly redirects a url to another url before sessions are established + /// </summary> + /// <param name="url">Url to redirect on</param> + /// <param name="redirecturl">Url to redirect to</param> + public Redirect(string url, string redirecturl) + { + Url = url; + RedirectUrl = new(redirecturl); + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Helpers/HttpHelpers.cs b/Net.Http/src/Helpers/HttpHelpers.cs new file mode 100644 index 0000000..9cceff1 --- /dev/null +++ b/Net.Http/src/Helpers/HttpHelpers.cs @@ -0,0 +1,445 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpHelpers.cs +* +* HttpHelpers.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +using VNLib.Net.Http.Core; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; + +namespace VNLib.Net.Http +{ + /// <summary> + /// Provides a set of HTTP helper functions + /// </summary> + public static partial class HttpHelpers + { + /// <summary> + /// Carrage return + line feed characters used within the VNLib.Net.Http namespace to delimit http messages/lines + /// </summary> + public const string CRLF = "\r\n"; + + public const string WebsocketRFC4122Guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + public const string EVENT_STREAM_ACCEPT_TYPE = "text/event-stream"; + + /// <summary> + /// Extended <see cref="HttpRequestHeader"/> for origin header, DO NOT USE IN <see cref="WebHeaderCollection"/> + /// </summary> + internal const HttpRequestHeader Origin = (HttpRequestHeader)42; + /// <summary> + /// Extended <see cref="HttpRequestHeader"/> for Content-Disposition, DO NOT USE IN <see cref="WebHeaderCollection"/> + /// </summary> + internal const HttpRequestHeader ContentDisposition = (HttpRequestHeader)41; + + private static readonly Regex HttpRequestBuilderRegex = new("(?<=[a-z])([A-Z])", RegexOptions.Compiled); + + /* + * Provides a hashable lookup table from a method string's hashcode to output + * an HttpMethod enum value, + */ + + private static readonly IReadOnlyDictionary<int, HttpMethod> MethodHashLookup; + + /* + * Provides a constant lookup table from an MIME http request header string to a .NET + * enum value (with some extra support) + */ + + private static readonly IReadOnlyDictionary<string, HttpRequestHeader> RequestHeaderLookup = new Dictionary<string, HttpRequestHeader>() + { + {"CacheControl", HttpRequestHeader.CacheControl }, + {"Connection", HttpRequestHeader.Connection }, + {"Date", HttpRequestHeader.Date }, + {"Keep-Alive", HttpRequestHeader.KeepAlive }, + {"Pragma", HttpRequestHeader.Pragma }, + {"Trailer", HttpRequestHeader.Trailer }, + {"Transfer-Encoding", HttpRequestHeader.TransferEncoding }, + {"Upgrade", HttpRequestHeader.Upgrade }, + {"Via", HttpRequestHeader.Via }, + {"Warning", HttpRequestHeader.Warning }, + {"Allow", HttpRequestHeader.Allow }, + {"Content-Length", HttpRequestHeader.ContentLength }, + {"Content-Type", HttpRequestHeader.ContentType }, + {"Content-Encoding", HttpRequestHeader.ContentEncoding }, + {"Content-Language", HttpRequestHeader.ContentLanguage }, + {"Content-Location", HttpRequestHeader.ContentLocation }, + {"Content-Md5", HttpRequestHeader.ContentMd5 }, + {"Content-Range", HttpRequestHeader.ContentRange }, + {"Expires", HttpRequestHeader.Expires }, + {"Last-Modified", HttpRequestHeader.LastModified }, + {"Accept", HttpRequestHeader.Accept }, + {"Accept-Charset", HttpRequestHeader.AcceptCharset }, + {"Accept-Encoding", HttpRequestHeader.AcceptEncoding }, + {"Accept-Language", HttpRequestHeader.AcceptLanguage }, + {"Authorization", HttpRequestHeader.Authorization }, + {"Cookie", HttpRequestHeader.Cookie }, + {"Expect", HttpRequestHeader.Expect }, + {"From", HttpRequestHeader.From }, + {"Host", HttpRequestHeader.Host }, + {"IfMatch", HttpRequestHeader.IfMatch }, + {"If-Modified-Since", HttpRequestHeader.IfModifiedSince }, + {"If-None-Match", HttpRequestHeader.IfNoneMatch }, + {"If-Range", HttpRequestHeader.IfRange }, + {"If-Unmodified-Since", HttpRequestHeader.IfUnmodifiedSince }, + {"MaxForwards", HttpRequestHeader.MaxForwards }, + {"Proxy-Authorization", HttpRequestHeader.ProxyAuthorization }, + {"Referer", HttpRequestHeader.Referer }, + {"Range", HttpRequestHeader.Range }, + {"Te", HttpRequestHeader.Te }, + {"Translate", HttpRequestHeader.Translate }, + {"User-Agent", HttpRequestHeader.UserAgent }, + //Custom request headers + { "Content-Disposition", ContentDisposition }, + { "origin", Origin } + }; + + /* + * Provides a lookup table for request header hashcodes (that are hashed in + * the static constructor) to ouput an http request header enum value from + * a header string's hashcode (allows for spans to produce an enum value + * during request parsing) + * + */ + private static readonly IReadOnlyDictionary<int, HttpRequestHeader> RequestHeaderHashLookup; + + /* + * Provides a constant lookup table for http version hashcodes to an http + * version enum value + */ + private static readonly IReadOnlyDictionary<int, HttpVersion> VersionHashLookup = new Dictionary<int, HttpVersion>() + { + { string.GetHashCode("HTTP/0.9", StringComparison.OrdinalIgnoreCase), HttpVersion.Http09 }, + { string.GetHashCode("HTTP/1.0", StringComparison.OrdinalIgnoreCase), HttpVersion.Http1 }, + { string.GetHashCode("HTTP/1.1", StringComparison.OrdinalIgnoreCase), HttpVersion.Http11 }, + { string.GetHashCode("HTTP/2.0", StringComparison.OrdinalIgnoreCase), HttpVersion.Http2 } + }; + + + //Pre-compiled strings for all status codes for http 1, 1.1, and 2 + private static readonly IReadOnlyDictionary<HttpStatusCode, string> V1_STAUTS_CODES; + private static readonly IReadOnlyDictionary<HttpStatusCode, string> V1_1_STATUS_CODES; + private static readonly IReadOnlyDictionary<HttpStatusCode, string> V2_STAUTS_CODES; + + static HttpHelpers() + { + { + //Setup status code dict + Dictionary<HttpStatusCode, string> v1status = new(); + Dictionary<HttpStatusCode, string> v11status = new(); + Dictionary<HttpStatusCode, string> v2status = new(); + //Get all status codes + foreach (HttpStatusCode code in Enum.GetValues<HttpStatusCode>()) + { + //Use a regex to write the status code value as a string + v1status[code] = $"HTTP/1.0 {(int)code} {HttpRequestBuilderRegex.Replace(code.ToString(), " $1")}"; + v11status[code] = $"HTTP/1.1 {(int)code} {HttpRequestBuilderRegex.Replace(code.ToString(), " $1")}"; + v2status[code] = $"HTTP/2.0 {(int)code} {HttpRequestBuilderRegex.Replace(code.ToString(), " $1")}"; + } + //Store as readonly + V1_STAUTS_CODES = v1status; + V1_1_STATUS_CODES = v11status; + V2_STAUTS_CODES = v2status; + } + { + /* + * Http methods are hashed at runtime using the HttpMethod enum + * values, purley for compatability and automation + */ + Dictionary<int, HttpMethod> methods = new(); + //Add all HTTP methods + foreach (HttpMethod method in Enum.GetValues<HttpMethod>()) + { + //Exclude the not supported method + if (method == HttpMethod.NOT_SUPPORTED) + { + continue; + } + //Store method string's hashcode for faster lookups + methods[string.GetHashCode(method.ToString(), StringComparison.OrdinalIgnoreCase)] = method; + } + MethodHashLookup = methods; + } + { + /* + * Pre-compute common headers + */ + Dictionary<int, HttpRequestHeader> requestHeaderHashes = new(); + //Add all HTTP methods + foreach (string headerValue in RequestHeaderLookup.Keys) + { + //Compute the hashcode for the header value + int hashCode = string.GetHashCode(headerValue, StringComparison.OrdinalIgnoreCase); + //Store the http header enum value with the hash-code of the string of said header + requestHeaderHashes[hashCode] = RequestHeaderLookup[headerValue]; + } + RequestHeaderHashLookup = requestHeaderHashes; + } + } + + + /// <summary> + /// Returns an http formatted content type string of a specified content type + /// </summary> + /// <param name="type">Contenty type</param> + /// <returns>Http acceptable string representing a content type</returns> + /// <exception cref="KeyNotFoundException"></exception> + public static string GetContentTypeString(ContentType type) => CtToMime[type]; + /// <summary> + /// Returns the <see cref="ContentType"/> enum value from the MIME string + /// </summary> + /// <param name="type">Content type from request</param> + /// <returns><see cref="ContentType"/> of request, <see cref="ContentType.NonSupported"/> if unknown</returns> + public static ContentType GetContentType(string type) => MimeToCt.GetValueOrDefault(type, ContentType.NonSupported); + //Cache control string using mdn reference + //https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control + /// <summary> + /// Builds a Cache-Control MIME content header from the specified flags + /// </summary> + /// <param name="type">The cache type/mode</param> + /// <param name="maxAge">The max-age (time in seconds) argument</param> + /// <param name="immutable">Sets the immutable argument</param> + /// <returns>The string representation of the Cache-Control header</returns> + public static string GetCacheString(CacheType type, int maxAge = 0, bool immutable = false) + { + //Rent a buffer to write header to + Span<char> buffer = stackalloc char[128]; + //Get buffer writer for cache header + ForwardOnlyWriter<char> sb = new(buffer); + if ((type & CacheType.NoCache) > 0) + { + sb.Append("no-cache, "); + } + if ((type & CacheType.NoStore) > 0) + { + sb.Append("no-store, "); + } + if ((type & CacheType.Public) > 0) + { + sb.Append("public, "); + } + if ((type & CacheType.Private) > 0) + { + sb.Append("private, "); + } + if ((type & CacheType.Revalidate) > 0) + { + sb.Append("must-revalidate, "); + } + if (immutable) + { + sb.Append("immutable, "); + } + sb.Append("max-age="); + sb.Append(maxAge); + return sb.ToString(); + } + /// <summary> + /// Builds a Cache-Control MIME content header from the specified flags + /// </summary> + /// <param name="type">The cache type/mode</param> + /// <param name="maxAge">The max-age argument</param> + /// <param name="immutable">Sets the immutable argument</param> + /// <returns>The string representation of the Cache-Control header</returns> + public static string GetCacheString(CacheType type, TimeSpan maxAge, bool immutable = false) => GetCacheString(type, (int)maxAge.TotalSeconds, immutable); + /// <summary> + /// Returns an enum value of an httpmethod of an http request method string + /// </summary> + /// <param name="smethod">Http acceptable method type string</param> + /// <returns>Request method, <see cref="HttpMethod.NOT_SUPPORTED"/> if method is malformatted or unsupported</returns> + /// <exception cref="ArgumentNullException"></exception> + public static HttpMethod GetRequestMethod(ReadOnlySpan<char> smethod) + { + //Get the hashcode for the method "string" + int hashCode = string.GetHashCode(smethod, StringComparison.OrdinalIgnoreCase); + //run the lookup and return not supported if the method was not found + return MethodHashLookup.GetValueOrDefault(hashCode, HttpMethod.NOT_SUPPORTED); + } + /// <summary> + /// Compares the first 3 bytes of IPV4 ip address or the first 6 bytes of a IPV6. Can be used to determine if the address is local to another address + /// </summary> + /// <param name="first">Address to be compared</param> + /// <param name="other">Address to be comared to first address</param> + /// <returns>True if first 2 bytes of each address match (Big Endian)</returns> + public static bool IsLocalSubnet(this IPAddress first, IPAddress other) + { + if(first.AddressFamily != other.AddressFamily) + { + return false; + } + switch (first.AddressFamily) + { + case AddressFamily.InterNetwork: + { + //Alloc buffers 4 bytes for IPV4 + Span<byte> firstBytes = stackalloc byte[4]; + Span<byte> otherBytes = stackalloc byte[4]; + //Write address's to the buffers + if (first.TryWriteBytes(firstBytes, out _) && other.TryWriteBytes(otherBytes, out _)) + { + //Compare the first 3 bytes of the first address to the second address + return firstBytes.StartsWith(otherBytes[..3]); + } + } + break; + case AddressFamily.InterNetworkV6: + { + //Alloc buffers 8 bytes for IPV6 + Span<byte> firstBytes = stackalloc byte[8]; + Span<byte> otherBytes = stackalloc byte[8]; + //Write address's to the buffers + if (first.TryWriteBytes(firstBytes, out _) && other.TryWriteBytes(otherBytes, out _)) + { + //Compare the first 6 bytes of the first address to the second address + return firstBytes.StartsWith(otherBytes[..6]); + } + } + break; + } + return false; + } + /// <summary> + /// Selects a <see cref="ContentType"/> for a given file extension + /// </summary> + /// <param name="path">Path (including extension) of a file</param> + /// <returns><see cref="ContentType"/> of file. Returns <see cref="ContentType.Binary"/> if extension is unknown</returns> + public static ContentType GetContentTypeFromFile(ReadOnlySpan<char> path) + { + //Get the file's extension + ReadOnlySpan<char> extention = Path.GetExtension(path); + //Trim leading . + extention = extention.Trim('.'); + //If the extension is defined, perform a lookup, otherwise return the default + return ExtensionToCt.GetValueOrDefault(extention.ToString(), ContentType.Binary); + } + /// <summary> + /// Selects a runtime compiled <see cref="string"/> matching the given <see cref="HttpStatusCode"/> and <see cref="HttpVersion"/> + /// </summary> + /// <param name="version">Version of the response string</param> + /// <param name="code">Status code of the response</param> + /// <returns>The HTTP response status line matching the code and version</returns> + public static string GetResponseString(HttpVersion version, HttpStatusCode code) + { + return version switch + { + HttpVersion.Http1 => V1_STAUTS_CODES[code], + HttpVersion.Http2 => V2_STAUTS_CODES[code], + _ => V1_1_STATUS_CODES[code], + }; + } + + /// <summary> + /// Parses the mime Content-Type header value into its sub-components + /// </summary> + /// <param name="header">The Content-Type header value field</param> + /// <param name="ContentType">The mime content type field</param> + /// <param name="Charset">The mime charset</param> + /// <param name="Boundry">The multi-part form boundry parameter</param> + /// <returns>True if parsing the content type succeded, false otherwise</returns> + public static bool TryParseContentType(string header, out string? ContentType, out string? Charset, out string? Boundry) + { + try + { + //Parse content type + System.Net.Mime.ContentType ctype = new(header); + Boundry = ctype.Boundary; + Charset = ctype.CharSet; + ContentType = ctype.MediaType; + return true; + } + catch +//Disable warning for not using the exception, intended behavior +#pragma warning disable ERP022 // Unobserved exception in a generic exception handler. + { + ContentType = Charset = Boundry = null; + //Invalid content type header value + } +#pragma warning restore ERP022 // Unobserved exception in a generic exception handler. + return false; + } + + /// <summary> + /// Parses a standard HTTP Content disposition header into its sub-components, type, name, filename (optional) + /// </summary> + /// <param name="header">The buffer containing the Content-Disposition header value only</param> + /// <param name="type">The mime form type</param> + /// <param name="name">The mime name argument</param> + /// <param name="fileName">The mime filename</param> + public static void ParseDisposition(ReadOnlySpan<char> header, out string? type, out string? name, out string? fileName) + { + //First parameter should be the type argument + type = header.SliceBeforeParam(';').Trim().ToString(); + //Set defaults for name and filename + name = fileName = null; + //get the name parameter + ReadOnlySpan<char> nameSpan = header.SliceAfterParam("name=\""); + if (!nameSpan.IsEmpty) + { + //Capture the name parameter value and trim it up + name = nameSpan.SliceBeforeParam('"').Trim().ToString(); + } + //Check for the filename parameter + ReadOnlySpan<char> fileNameSpan = header.SliceAfterParam("filename=\""); + if (!fileNameSpan.IsEmpty) + { + //Capture the name parameter value and trim it up + fileName = fileNameSpan.SliceBeforeParam('"').Trim().ToString(); + } + } + + /// <summary> + /// Performs a lookup of the specified header name to get the <see cref="HttpRequestHeader"/> enum value + /// </summary> + /// <param name="requestHeaderName">The value of the HTTP request header to compute</param> + /// <returns>The <see cref="HttpRequestHeader"/> enum value of the header, or 255 if not found</returns> + internal static HttpRequestHeader GetRequestHeaderEnumFromValue(ReadOnlySpan<char> requestHeaderName) + { + //Compute the hashcode from the header name + int hashcode = string.GetHashCode(requestHeaderName, StringComparison.OrdinalIgnoreCase); + //perform lookup + return RequestHeaderHashLookup.GetValueOrDefault(hashcode, (HttpRequestHeader)255); + } + + /// <summary> + /// Gets the <see cref="HttpVersion"/> enum value from the version string + /// </summary> + /// <param name="httpVersion">The http header version string</param> + /// <returns>The <see cref="HttpVersion"/> enum value, or + /// <see cref="HttpVersion.NotSupported"/> if the version could not be + /// determined + /// </returns> + public static HttpVersion ParseHttpVersion(ReadOnlySpan<char> httpVersion) + { + //Get the hashcode for the http version "string" + int hashCode = string.GetHashCode(httpVersion.Trim(), StringComparison.OrdinalIgnoreCase); + //return the version that matches the hashcode, or return unsupported of not found + return VersionHashLookup.GetValueOrDefault(hashCode, HttpVersion.NotSupported); + } + } +}
\ No newline at end of file diff --git a/Net.Http/src/Helpers/MimeLookups.cs b/Net.Http/src/Helpers/MimeLookups.cs new file mode 100644 index 0000000..03bc59d --- /dev/null +++ b/Net.Http/src/Helpers/MimeLookups.cs @@ -0,0 +1,3237 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: MimeLookups.cs +* +* MimeLookups.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace VNLib.Net.Http +{ + public static partial class HttpHelpers + { + //Content type lookup dict + private static readonly IReadOnlyDictionary<ContentType, string> CtToMime = new Dictionary<ContentType, string>() + { + { ContentType.NonSupported, "application/octet-stream" }, + { ContentType.UrlEncoded, "application/x-www-form-urlencoded" }, + { ContentType.MultiPart, "multipart/form-data" }, + { ContentType.Aab, "application/x-authorware-bin" }, + { ContentType.Aac, "audio/x-aac" }, + { ContentType.Aam, "application/x-authorware-map" }, + { ContentType.Aas, "application/x-authorware-seg" }, + { ContentType.Abw, "application/x-abiword" }, + { ContentType.Ac, "application/pkix-attr-cert" }, + { ContentType.Acc, "application/vnd.americandynamics.acc" }, + { ContentType.Ace, "application/x-ace-compressed" }, + { ContentType.Acu, "application/vnd.acucobol" }, + { ContentType.Acutc, "application/vnd.acucorp" }, + { ContentType.Adp, "audio/adpcm" }, + { ContentType.Aep, "application/vnd.audiograph" }, + { ContentType.Afm, "application/x-font-type1" }, + { ContentType.Afp, "application/vnd.ibm.modcap" }, + { ContentType.Ahead, "application/vnd.ahead.space" }, + { ContentType.Ai, "application/postscript" }, + { ContentType.Aif, "audio/x-aiff" }, + { ContentType.Aifc, "audio/x-aiff" }, + { ContentType.Aiff, "audio/x-aiff" }, + { ContentType.Air, "application/vnd.adobe.air-application-installer-package+zip" }, + { ContentType.Ait, "application/vnd.dvb.ait" }, + { ContentType.Ami, "application/vnd.amiga.ami" }, + { ContentType.Amr, "audio/amr" }, + { ContentType.Apk, "application/vnd.android.package-archive" }, + { ContentType.Apng, "image/apng" }, + { ContentType.Appcache, "text/cache-manifest" }, + { ContentType.Apr, "application/vnd.lotus-approach" }, + { ContentType.Arc, "application/x-freearc" }, + { ContentType.Arj, "application/x-arj" }, + { ContentType.Asc, "application/pgp-signature" }, + { ContentType.Asf, "video/x-ms-asf" }, + { ContentType.Asm, "text/x-asm" }, + { ContentType.Aso, "application/vnd.accpac.simply.aso" }, + { ContentType.Asx, "video/x-ms-asf" }, + { ContentType.Atc, "application/vnd.acucorp" }, + { ContentType.Atom, "application/atom+xml" }, + { ContentType.Atomcat, "application/atomcat+xml" }, + { ContentType.Atomsvc, "application/atomsvc+xml" }, + { ContentType.Atx, "application/vnd.antix.game-component" }, + { ContentType.Au, "audio/basic" }, + { ContentType.Avi, "video/x-msvideo" }, + { ContentType.Avif, "image/avif" }, + { ContentType.Aw, "application/applixware" }, + { ContentType.Azf, "application/vnd.airzip.filesecure.azf" }, + { ContentType.Azs, "application/vnd.airzip.filesecure.azs" }, + { ContentType.Azv, "image/vnd.airzip.accelerator.azv" }, + { ContentType.Azw, "application/vnd.amazon.ebook" }, + { ContentType.B16, "image/vnd.pco.b16" }, + { ContentType.Bat, "application/x-msdownload" }, + { ContentType.Bcpio, "application/x-bcpio" }, + { ContentType.Bdf, "application/x-font-bdf" }, + { ContentType.Bdm, "application/vnd.syncml.dm+wbxml" }, + { ContentType.Bdoc, "application/bdoc" }, + { ContentType.Bed, "application/vnd.realvnc.bed" }, + { ContentType.Bh2, "application/vnd.fujitsu.oasysprs" }, + { ContentType.Binary, "application/octet-stream" }, + { ContentType.Blb, "application/x-blorb" }, + { ContentType.Blorb, "application/x-blorb" }, + { ContentType.Bmi, "application/vnd.bmi" }, + { ContentType.Bmml, "application/vnd.balsamiq.bmml+xml" }, + { ContentType.Bmp, "image/bmp" }, + { ContentType.Book, "application/vnd.framemaker" }, + { ContentType.Box, "application/vnd.previewsystems.box" }, + { ContentType.Boz, "application/x-bzip2" }, + { ContentType.Bpk, "application/octet-stream" }, + { ContentType.Bsp, "model/vnd.valve.source.compiled-map" }, + { ContentType.Btif, "image/prs.btif" }, + { ContentType.Buffer, "application/octet-stream" }, + { ContentType.Bz, "application/x-bzip" }, + { ContentType.Bz2, "application/x-bzip2" }, + { ContentType.C, "text/x-c" }, + { ContentType.C11amc, "application/vnd.cluetrust.cartomobile-config" }, + { ContentType.C11amz, "application/vnd.cluetrust.cartomobile-config-pkg" }, + { ContentType.C4d, "application/vnd.clonk.c4group" }, + { ContentType.C4f, "application/vnd.clonk.c4group" }, + { ContentType.C4g, "application/vnd.clonk.c4group" }, + { ContentType.C4p, "application/vnd.clonk.c4group" }, + { ContentType.C4u, "application/vnd.clonk.c4group" }, + { ContentType.Cab, "application/vnd.ms-cab-compressed" }, + { ContentType.Caf, "audio/x-caf" }, + { ContentType.Cap, "application/vnd.tcpdump.pcap" }, + { ContentType.Car, "application/vnd.curl.car" }, + { ContentType.Cat, "application/vnd.ms-pki.seccat" }, + { ContentType.Cb7, "application/x-cbr" }, + { ContentType.Cba, "application/x-cbr" }, + { ContentType.Cbr, "application/x-cbr" }, + { ContentType.Cbt, "application/x-cbr" }, + { ContentType.Cbz, "application/x-cbr" }, + { ContentType.Cc, "text/x-c" }, + { ContentType.Cco, "application/x-cocoa" }, + { ContentType.Cct, "application/x-director" }, + { ContentType.Ccxml, "application/ccxml+xml" }, + { ContentType.Cdbcmsg, "application/vnd.contact.cmsg" }, + { ContentType.Cdf, "application/x-netcdf" }, + { ContentType.Cdfx, "application/cdfx+xml" }, + { ContentType.Cdkey, "application/vnd.mediastation.cdkey" }, + { ContentType.Cdmia, "application/cdmi-capability" }, + { ContentType.Cdmic, "application/cdmi-container" }, + { ContentType.Cdmid, "application/cdmi-domain" }, + { ContentType.Cdmio, "application/cdmi-object" }, + { ContentType.Cdmiq, "application/cdmi-queue" }, + { ContentType.Cdx, "chemical/x-cdx" }, + { ContentType.Cdxml, "application/vnd.chemdraw+xml" }, + { ContentType.Cdy, "application/vnd.cinderella" }, + { ContentType.Cer, "application/pkix-cert" }, + { ContentType.Cfs, "application/x-cfs-compressed" }, + { ContentType.Cgm, "image/cgm" }, + { ContentType.Chat, "application/x-chat" }, + { ContentType.Chm, "application/vnd.ms-htmlhelp" }, + { ContentType.Chrt, "application/vnd.kde.kchart" }, + { ContentType.Cif, "chemical/x-cif" }, + { ContentType.Cii, "application/vnd.anser-web-certificate-issue-initiation" }, + { ContentType.Cil, "application/vnd.ms-artgalry" }, + { ContentType.Cjs, "application/node" }, + { ContentType.Cla, "application/vnd.claymore" }, + { ContentType.Clkk, "application/vnd.crick.clicker.keyboard" }, + { ContentType.Clkp, "application/vnd.crick.clicker.palette" }, + { ContentType.Clkt, "application/vnd.crick.clicker.template" }, + { ContentType.Clkw, "application/vnd.crick.clicker.wordbank" }, + { ContentType.Clkx, "application/vnd.crick.clicker" }, + { ContentType.Clp, "application/x-msclip" }, + { ContentType.Cmc, "application/vnd.cosmocaller" }, + { ContentType.Cmdf, "chemical/x-cmdf" }, + { ContentType.Cml, "chemical/x-cml" }, + { ContentType.Cmp, "application/vnd.yellowriver-custom-menu" }, + { ContentType.Cmx, "image/x-cmx" }, + { ContentType.Cod, "application/vnd.rim.cod" }, + { ContentType.Coffee, "text/coffeescript" }, + { ContentType.Com, "application/x-msdownload" }, + { ContentType.Conf, "text/plain" }, + { ContentType.Cpio, "application/x-cpio" }, + { ContentType.Cpp, "text/x-c" }, + { ContentType.Cpt, "application/mac-compactpro" }, + { ContentType.Crd, "application/x-mscardfile" }, + { ContentType.Crl, "application/pkix-crl" }, + { ContentType.Crt, "application/x-x509-ca-cert" }, + { ContentType.Crx, "application/x-chrome-extension" }, + { ContentType.Csh, "application/x-csh" }, + { ContentType.Csl, "application/vnd.citationstyles.style+xml" }, + { ContentType.Csml, "chemical/x-csml" }, + { ContentType.Csp, "application/vnd.commonspace" }, + { ContentType.Css, "text/css" }, + { ContentType.Cst, "application/x-director" }, + { ContentType.Csv, "text/csv" }, + { ContentType.Cu, "application/cu-seeme" }, + { ContentType.Curl, "text/vnd.curl" }, + { ContentType.Cww, "application/prs.cww" }, + { ContentType.Cxt, "application/x-director" }, + { ContentType.Cxx, "text/x-c" }, + { ContentType.Dae, "model/vnd.collada+xml" }, + { ContentType.Daf, "application/vnd.mobius.daf" }, + { ContentType.Dart, "application/vnd.dart" }, + { ContentType.Dataless, "application/vnd.fdsn.seed" }, + { ContentType.Davmount, "application/davmount+xml" }, + { ContentType.Dbf, "application/vnd.dbf" }, + { ContentType.Dbk, "application/docbook+xml" }, + { ContentType.Dcr, "application/x-director" }, + { ContentType.Dcurl, "text/vnd.curl.dcurl" }, + { ContentType.Dd2, "application/vnd.oma.dd2+xml" }, + { ContentType.Ddd, "application/vnd.fujixerox.ddd" }, + { ContentType.Ddf, "application/vnd.syncml.dmddf+xml" }, + { ContentType.Dds, "image/vnd.ms-dds" }, + { ContentType.Deb, "application/octet-stream" }, + { ContentType.Def, "text/plain" }, + { ContentType.Deploy, "application/octet-stream" }, + { ContentType.Der, "application/x-x509-ca-cert" }, + { ContentType.Dfac, "application/vnd.dreamfactory" }, + { ContentType.Dgc, "application/x-dgc-compressed" }, + { ContentType.Dic, "text/x-c" }, + { ContentType.Dir, "application/x-director" }, + { ContentType.Dis, "application/vnd.mobius.dis" }, + { ContentType.Dist, "application/octet-stream" }, + { ContentType.Distz, "application/octet-stream" }, + { ContentType.Djv, "image/vnd.djvu" }, + { ContentType.Djvu, "image/vnd.djvu" }, + { ContentType.Dll, "application/octet-stream" }, + { ContentType.Dmg, "application/octet-stream" }, + { ContentType.Dmp, "application/vnd.tcpdump.pcap" }, + { ContentType.Dms, "application/octet-stream" }, + { ContentType.Dna, "application/vnd.dna" }, + { ContentType.Doc, "application/msword" }, + { ContentType.Docm, "application/vnd.ms-word.document.macroenabled.12" }, + { ContentType.Docx, "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { ContentType.Dot, "application/msword" }, + { ContentType.Dotm, "application/vnd.ms-word.template.macroenabled.12" }, + { ContentType.Dotx, "application/vnd.openxmlformats-officedocument.wordprocessingml.template" }, + { ContentType.Dp, "application/vnd.osgi.dp" }, + { ContentType.Dpg, "application/vnd.dpgraph" }, + { ContentType.Dra, "audio/vnd.dra" }, + { ContentType.Drle, "image/dicom-rle" }, + { ContentType.Dsc, "text/prs.lines.tag" }, + { ContentType.Dssc, "application/dssc+der" }, + { ContentType.Dtb, "application/x-dtbook+xml" }, + { ContentType.Dtd, "application/xml-dtd" }, + { ContentType.Dts, "audio/vnd.dts" }, + { ContentType.Dtshd, "audio/vnd.dts.hd" }, + { ContentType.Dump, "application/octet-stream" }, + { ContentType.Dvb, "video/vnd.dvb.file" }, + { ContentType.Dvi, "application/x-dvi" }, + { ContentType.Dwd, "application/atsc-dwd+xml" }, + { ContentType.Dwf, "model/vnd.dwf" }, + { ContentType.Dwg, "image/vnd.dwg" }, + { ContentType.Dxf, "image/vnd.dxf" }, + { ContentType.Dxp, "application/vnd.spotfire.dxp" }, + { ContentType.Dxr, "application/x-director" }, + { ContentType.Ear, "application/java-archive" }, + { ContentType.Ecma, "application/ecmascript" }, + { ContentType.Edm, "application/vnd.novadigm.edm" }, + { ContentType.Edx, "application/vnd.novadigm.edx" }, + { ContentType.Efif, "application/vnd.picsel" }, + { ContentType.Ei6, "application/vnd.pg.osasli" }, + { ContentType.Elc, "application/octet-stream" }, + { ContentType.Emf, "application/x-msmetafile" }, + { ContentType.Eml, "message/rfc822" }, + { ContentType.Emma, "application/emma+xml" }, + { ContentType.Emz, "application/x-msmetafile" }, + { ContentType.Eol, "audio/vnd.digital-winds" }, + { ContentType.Eot, "application/vnd.ms-fontobject" }, + { ContentType.Eps, "application/postscript" }, + { ContentType.Epub, "application/epub+zip" }, + { ContentType.Es, "application/ecmascript" }, + { ContentType.Es3, "application/vnd.eszigno3+xml" }, + { ContentType.Esa, "application/vnd.osgi.subsystem" }, + { ContentType.Esf, "application/vnd.epson.esf" }, + { ContentType.Et3, "application/vnd.eszigno3+xml" }, + { ContentType.Etx, "text/x-setext" }, + { ContentType.Eva, "application/x-eva" }, + { ContentType.Evy, "application/x-envoy" }, + { ContentType.Exe, "application/octet-stream" }, + { ContentType.Exi, "application/exi" }, + { ContentType.Exp, "application/express" }, + { ContentType.Exr, "image/aces" }, + { ContentType.Ext, "application/vnd.novadigm.ext" }, + { ContentType.Ez, "application/andrew-inset" }, + { ContentType.Ez2, "application/vnd.ezpix-album" }, + { ContentType.Ez3, "application/vnd.ezpix-package" }, + { ContentType.F, "text/x-fortran" }, + { ContentType.F4v, "video/x-f4v" }, + { ContentType.Fortran, "text/x-fortran" }, + { ContentType.F90, "text/x-fortran" }, + { ContentType.Fbs, "image/vnd.fastbidsheet" }, + { ContentType.Fcdt, "application/vnd.adobe.formscentral.fcdt" }, + { ContentType.Fcs, "application/vnd.isac.fcs" }, + { ContentType.Fdf, "application/vnd.fdf" }, + { ContentType.Fdt, "application/fdt+xml" }, + { ContentType.Fg5, "application/vnd.fujitsu.oasysgp" }, + { ContentType.Fgd, "application/x-director" }, + { ContentType.Fh, "image/x-freehand" }, + { ContentType.Fh4, "image/x-freehand" }, + { ContentType.Fh5, "image/x-freehand" }, + { ContentType.Fh7, "image/x-freehand" }, + { ContentType.Fhc, "image/x-freehand" }, + { ContentType.Fig, "application/x-xfig" }, + { ContentType.Fits, "image/fits" }, + { ContentType.Flac, "audio/x-flac" }, + { ContentType.Fli, "video/x-fli" }, + { ContentType.Flo, "application/vnd.micrografx.flo" }, + { ContentType.Flv, "video/x-flv" }, + { ContentType.Flw, "application/vnd.kde.kivio" }, + { ContentType.Flx, "text/vnd.fmi.flexstor" }, + { ContentType.Fly, "text/vnd.fly" }, + { ContentType.Fm, "application/vnd.framemaker" }, + { ContentType.Fnc, "application/vnd.frogans.fnc" }, + { ContentType.Fo, "application/vnd.software602.filler.form+xml" }, + { ContentType.For, "text/x-fortran" }, + { ContentType.Fpx, "image/vnd.fpx" }, + { ContentType.Frame, "application/vnd.framemaker" }, + { ContentType.Fsc, "application/vnd.fsc.weblaunch" }, + { ContentType.Fst, "image/vnd.fst" }, + { ContentType.Ftc, "application/vnd.fluxtime.clip" }, + { ContentType.Fti, "application/vnd.anser-web-funds-transfer-initiation" }, + { ContentType.Fvt, "video/vnd.fvt" }, + { ContentType.Fxp, "application/vnd.adobe.fxp" }, + { ContentType.Fxpl, "application/vnd.adobe.fxp" }, + { ContentType.Fzs, "application/vnd.fuzzysheet" }, + { ContentType.G2w, "application/vnd.geoplan" }, + { ContentType.G3, "image/g3fax" }, + { ContentType.G3w, "application/vnd.geospace" }, + { ContentType.Gac, "application/vnd.groove-account" }, + { ContentType.Gam, "application/x-tads" }, + { ContentType.Gbr, "application/rpki-ghostbusters" }, + { ContentType.Gca, "application/x-gca-compressed" }, + { ContentType.Gdl, "model/vnd.gdl" }, + { ContentType.Gdoc, "application/vnd.google-apps.document" }, + { ContentType.Geo, "application/vnd.dynageo" }, + { ContentType.Geojson, "application/geo+json" }, + { ContentType.Gex, "application/vnd.geometry-explorer" }, + { ContentType.Ggb, "application/vnd.geogebra.file" }, + { ContentType.Ggt, "application/vnd.geogebra.tool" }, + { ContentType.Ghf, "application/vnd.groove-help" }, + { ContentType.Gif, "image/gif" }, + { ContentType.Gim, "application/vnd.groove-identity-message" }, + { ContentType.Glb, "model/gltf-binary" }, + { ContentType.Gltf, "model/gltf+json" }, + { ContentType.Gml, "application/gml+xml" }, + { ContentType.Gmx, "application/vnd.gmx" }, + { ContentType.Gnumeric, "application/x-gnumeric" }, + { ContentType.Gph, "application/vnd.flographit" }, + { ContentType.Gpx, "application/gpx+xml" }, + { ContentType.Gqf, "application/vnd.grafeq" }, + { ContentType.Gqs, "application/vnd.grafeq" }, + { ContentType.Gram, "application/srgs" }, + { ContentType.Gramps, "application/x-gramps-xml" }, + { ContentType.Gre, "application/vnd.geometry-explorer" }, + { ContentType.Grv, "application/vnd.groove-injector" }, + { ContentType.Grxml, "application/srgs+xml" }, + { ContentType.Gsf, "application/x-font-ghostscript" }, + { ContentType.Gsheet, "application/vnd.google-apps.spreadsheet" }, + { ContentType.Gslides, "application/vnd.google-apps.presentation" }, + { ContentType.Gtar, "application/x-gtar" }, + { ContentType.Gtm, "application/vnd.groove-tool-message" }, + { ContentType.Gtw, "model/vnd.gtw" }, + { ContentType.Gv, "text/vnd.graphviz" }, + { ContentType.Gxf, "application/gxf" }, + { ContentType.Gxt, "application/vnd.geonext" }, + { ContentType.Gz, "application/gzip" }, + { ContentType.H, "text/x-c" }, + { ContentType.H261, "video/h261" }, + { ContentType.H263, "video/h263" }, + { ContentType.H264, "video/h264" }, + { ContentType.Hal, "application/vnd.hal+xml" }, + { ContentType.Hbci, "application/vnd.hbci" }, + { ContentType.Hbs, "text/x-handlebars-template" }, + { ContentType.Hdd, "application/x-virtualbox-hdd" }, + { ContentType.Hdf, "application/x-hdf" }, + { ContentType.Heic, "image/heic" }, + { ContentType.Heics, "image/heic-sequence" }, + { ContentType.Heif, "image/heif" }, + { ContentType.Heifs, "image/heif-sequence" }, + { ContentType.Hej2, "image/hej2k" }, + { ContentType.Held, "application/atsc-held+xml" }, + { ContentType.Hh, "text/x-c" }, + { ContentType.Hjson, "application/hjson" }, + { ContentType.Hlp, "application/winhlp" }, + { ContentType.Hpgl, "application/vnd.hp-hpgl" }, + { ContentType.Hpid, "application/vnd.hp-hpid" }, + { ContentType.Hps, "application/vnd.hp-hps" }, + { ContentType.Hqx, "application/mac-binhex40" }, + { ContentType.Hsj2, "image/hsj2" }, + { ContentType.Htc, "text/x-component" }, + { ContentType.Htke, "application/vnd.kenameaapp" }, + { ContentType.Htm, "text/html" }, + { ContentType.Html, "text/html" }, + { ContentType.Hvd, "application/vnd.yamaha.hv-dic" }, + { ContentType.Hvp, "application/vnd.yamaha.hv-voice" }, + { ContentType.Hvs, "application/vnd.yamaha.hv-script" }, + { ContentType.I2g, "application/vnd.intergeo" }, + { ContentType.Icc, "application/vnd.iccprofile" }, + { ContentType.Ice, "x-conference/x-cooltalk" }, + { ContentType.Icm, "application/vnd.iccprofile" }, + { ContentType.Ico, "image/vnd.microsoft.icon" }, + { ContentType.Ics, "text/calendar" }, + { ContentType.Ief, "image/ief" }, + { ContentType.Ifb, "text/calendar" }, + { ContentType.Ifm, "application/vnd.shana.informed.formdata" }, + { ContentType.Iges, "model/iges" }, + { ContentType.Igl, "application/vnd.igloader" }, + { ContentType.Igm, "application/vnd.insors.igm" }, + { ContentType.Igs, "model/iges" }, + { ContentType.Igx, "application/vnd.micrografx.igx" }, + { ContentType.Iif, "application/vnd.shana.informed.interchange" }, + { ContentType.Img, "application/octet-stream" }, + { ContentType.Imp, "application/vnd.accpac.simply.imp" }, + { ContentType.Ims, "application/vnd.ms-ims" }, + { ContentType.Ini, "text/plain" }, + { ContentType.Ink, "application/inkml+xml" }, + { ContentType.Inkml, "application/inkml+xml" }, + { ContentType.Install, "application/x-install-instructions" }, + { ContentType.Iota, "application/vnd.astraea-software.iota" }, + { ContentType.Ipfix, "application/ipfix" }, + { ContentType.Ipk, "application/vnd.shana.informed.package" }, + { ContentType.Irm, "application/vnd.ibm.rights-management" }, + { ContentType.Irp, "application/vnd.irepository.package+xml" }, + { ContentType.Iso, "application/octet-stream" }, + { ContentType.Itp, "application/vnd.shana.informed.formtemplate" }, + { ContentType.Its, "application/its+xml" }, + { ContentType.Ivp, "application/vnd.immervision-ivp" }, + { ContentType.Ivu, "application/vnd.immervision-ivu" }, + { ContentType.Jad, "text/vnd.sun.j2me.app-descriptor" }, + { ContentType.Jade, "text/jade" }, + { ContentType.Jam, "application/vnd.jam" }, + { ContentType.Jar, "application/java-archive" }, + { ContentType.Jardiff, "application/x-java-archive-diff" }, + { ContentType.Java, "text/x-java-source" }, + { ContentType.Jhc, "image/jphc" }, + { ContentType.Jisp, "application/vnd.jisp" }, + { ContentType.Jls, "image/jls" }, + { ContentType.Jlt, "application/vnd.hp-jlyt" }, + { ContentType.Jng, "image/x-jng" }, + { ContentType.Jnlp, "application/x-java-jnlp-file" }, + { ContentType.Joda, "application/vnd.joost.joda-archive" }, + { ContentType.Jp2, "image/jp2" }, + { ContentType.Jpe, "image/jpeg" }, + { ContentType.Jpeg, "image/jpeg" }, + { ContentType.Jpf, "image/jpx" }, + { ContentType.Jpg, "image/jpeg" }, + { ContentType.Jpg2, "image/jp2" }, + { ContentType.Jpgm, "video/jpm" }, + { ContentType.Jpgv, "video/jpeg" }, + { ContentType.Jph, "image/jph" }, + { ContentType.Jpm, "image/jpm" }, + { ContentType.Jpx, "image/jpx" }, + { ContentType.Javascript, "application/javascript" }, + { ContentType.Json, "application/json" }, + { ContentType.Json5, "application/json5" }, + { ContentType.Jsonld, "application/ld+json" }, + { ContentType.Jsonml, "application/jsonml+json" }, + { ContentType.Jsx, "text/jsx" }, + { ContentType.Jxr, "image/jxr" }, + { ContentType.Jxra, "image/jxra" }, + { ContentType.Jxrs, "image/jxrs" }, + { ContentType.Jxs, "image/jxs" }, + { ContentType.Jxsc, "image/jxsc" }, + { ContentType.Jxsi, "image/jxsi" }, + { ContentType.Jxss, "image/jxss" }, + { ContentType.Kar, "audio/midi" }, + { ContentType.Karbon, "application/vnd.kde.karbon" }, + { ContentType.Kdbx, "application/x-keepass2" }, + { ContentType.Key, "application/vnd.apple.keynote" }, + { ContentType.Kfo, "application/vnd.kde.kformula" }, + { ContentType.Kia, "application/vnd.kidspiration" }, + { ContentType.Kml, "application/vnd.google-earth.kml+xml" }, + { ContentType.Kmz, "application/vnd.google-earth.kmz" }, + { ContentType.Kne, "application/vnd.kinar" }, + { ContentType.Knp, "application/vnd.kinar" }, + { ContentType.Kon, "application/vnd.kde.kontour" }, + { ContentType.Kpr, "application/vnd.kde.kpresenter" }, + { ContentType.Kpt, "application/vnd.kde.kpresenter" }, + { ContentType.Kpxx, "application/vnd.ds-keypoint" }, + { ContentType.Ksp, "application/vnd.kde.kspread" }, + { ContentType.Ktr, "application/vnd.kahootz" }, + { ContentType.Ktx, "image/ktx" }, + { ContentType.Ktx2, "image/ktx2" }, + { ContentType.Ktz, "application/vnd.kahootz" }, + { ContentType.Kwd, "application/vnd.kde.kword" }, + { ContentType.Kwt, "application/vnd.kde.kword" }, + { ContentType.Lasxml, "application/vnd.las.las+xml" }, + { ContentType.Latex, "application/x-latex" }, + { ContentType.Lbd, "application/vnd.llamagraphics.life-balance.desktop" }, + { ContentType.Lbe, "application/vnd.llamagraphics.life-balance.exchange+xml" }, + { ContentType.Les, "application/vnd.hhe.lesson-player" }, + { ContentType.Less, "text/less" }, + { ContentType.Lgr, "application/lgr+xml" }, + { ContentType.Lha, "application/x-lzh-compressed" }, + { ContentType.Link66, "application/vnd.route66.link66+xml" }, + { ContentType.List, "text/plain" }, + { ContentType.List3820, "application/vnd.ibm.modcap" }, + { ContentType.Listafp, "application/vnd.ibm.modcap" }, + { ContentType.Lnk, "application/x-ms-shortcut" }, + { ContentType.Log, "text/plain" }, + { ContentType.Lostxml, "application/lost+xml" }, + { ContentType.Lrf, "application/octet-stream" }, + { ContentType.Lrm, "application/vnd.ms-lrm" }, + { ContentType.Ltf, "application/vnd.frogans.ltf" }, + { ContentType.Lua, "text/x-lua" }, + { ContentType.Luac, "application/x-lua-bytecode" }, + { ContentType.Lvp, "audio/vnd.lucent.voice" }, + { ContentType.Lwp, "application/vnd.lotus-wordpro" }, + { ContentType.Lzh, "application/x-lzh-compressed" }, + { ContentType.M13, "application/x-msmediaview" }, + { ContentType.M14, "application/x-msmediaview" }, + { ContentType.M1v, "video/mpeg" }, + { ContentType.M21, "application/mp21" }, + { ContentType.M2a, "audio/mpeg" }, + { ContentType.M2v, "video/mpeg" }, + { ContentType.M3a, "audio/mpeg" }, + { ContentType.M3u, "audio/x-mpegurl" }, + { ContentType.M3u8, "application/vnd.apple.mpegurl" }, + { ContentType.M4a, "audio/mp4" }, + { ContentType.M4p, "application/mp4" }, + { ContentType.M4s, "video/iso.segment" }, + { ContentType.M4u, "video/vnd.mpegurl" }, + { ContentType.M4v, "video/x-m4v" }, + { ContentType.Ma, "application/mathematica" }, + { ContentType.Mads, "application/mads+xml" }, + { ContentType.Maei, "application/mmt-aei+xml" }, + { ContentType.Mag, "application/vnd.ecowin.chart" }, + { ContentType.Maker, "application/vnd.framemaker" }, + { ContentType.Man, "text/troff" }, + { ContentType.Manifest, "text/cache-manifest" }, + { ContentType.Map, "application/json" }, + { ContentType.Mar, "application/octet-stream" }, + { ContentType.Markdown, "text/markdown" }, + { ContentType.Mathml, "application/mathml+xml" }, + { ContentType.Mb, "application/mathematica" }, + { ContentType.Mbk, "application/vnd.mobius.mbk" }, + { ContentType.Mbox, "application/mbox" }, + { ContentType.Mc1, "application/vnd.medcalcdata" }, + { ContentType.Mcd, "application/vnd.mcd" }, + { ContentType.Mcurl, "text/vnd.curl.mcurl" }, + { ContentType.Md, "text/markdown" }, + { ContentType.Mdb, "application/x-msaccess" }, + { ContentType.Mdi, "image/vnd.ms-modi" }, + { ContentType.Mdx, "text/mdx" }, + { ContentType.Me, "text/troff" }, + { ContentType.Mesh, "model/mesh" }, + { ContentType.Meta4, "application/metalink4+xml" }, + { ContentType.Metalink, "application/metalink+xml" }, + { ContentType.Mets, "application/mets+xml" }, + { ContentType.Mfm, "application/vnd.mfmp" }, + { ContentType.Mft, "application/rpki-manifest" }, + { ContentType.Mgp, "application/vnd.osgeo.mapguide.package" }, + { ContentType.Mgz, "application/vnd.proteus.magazine" }, + { ContentType.Mid, "audio/midi" }, + { ContentType.Midi, "audio/midi" }, + { ContentType.Mie, "application/x-mie" }, + { ContentType.Mif, "application/vnd.mif" }, + { ContentType.Mime, "message/rfc822" }, + { ContentType.Mj2, "video/mj2" }, + { ContentType.Mjp2, "video/mj2" }, + { ContentType.Mjs, "application/javascript" }, + { ContentType.Mk3d, "video/x-matroska" }, + { ContentType.Mka, "audio/x-matroska" }, + { ContentType.Mkd, "text/x-markdown" }, + { ContentType.Mks, "video/x-matroska" }, + { ContentType.Mkv, "video/x-matroska" }, + { ContentType.Mlp, "application/vnd.dolby.mlp" }, + { ContentType.Mmd, "application/vnd.chipnuts.karaoke-mmd" }, + { ContentType.Mmf, "application/vnd.smaf" }, + { ContentType.Mml, "text/mathml" }, + { ContentType.Mmr, "image/vnd.fujixerox.edmics-mmr" }, + { ContentType.Mng, "video/x-mng" }, + { ContentType.Mny, "application/x-msmoney" }, + { ContentType.Mobi, "application/x-mobipocket-ebook" }, + { ContentType.Mods, "application/mods+xml" }, + { ContentType.Mov, "video/quicktime" }, + { ContentType.Movie, "video/x-sgi-movie" }, + { ContentType.Mp2, "audio/mpeg" }, + { ContentType.Mp21, "application/mp21" }, + { ContentType.Mp2a, "audio/mpeg" }, + { ContentType.Mp3, "audio/mp3" }, + { ContentType.Mp4, "video/mp4" }, + { ContentType.Mp4a, "audio/mp4" }, + { ContentType.Mp4s, "application/mp4" }, + { ContentType.Mp4v, "video/mp4" }, + { ContentType.Mpc, "application/vnd.mophun.certificate" }, + { ContentType.Mpd, "application/dash+xml" }, + { ContentType.Mpe, "video/mpeg" }, + { ContentType.Mpeg, "video/mpeg" }, + { ContentType.Mpg, "video/mpeg" }, + { ContentType.Mpg4, "video/mp4" }, + { ContentType.Mpga, "audio/mpeg" }, + { ContentType.Mpkg, "application/vnd.apple.installer+xml" }, + { ContentType.Mpm, "application/vnd.blueice.multipass" }, + { ContentType.Mpn, "application/vnd.mophun.application" }, + { ContentType.Mpp, "application/vnd.ms-project" }, + { ContentType.Mpt, "application/vnd.ms-project" }, + { ContentType.Mpy, "application/vnd.ibm.minipay" }, + { ContentType.Mqy, "application/vnd.mobius.mqy" }, + { ContentType.Mrc, "application/marc" }, + { ContentType.Mrcx, "application/marcxml+xml" }, + { ContentType.Ms, "text/troff" }, + { ContentType.Mscml, "application/mediaservercontrol+xml" }, + { ContentType.Mseed, "application/vnd.fdsn.mseed" }, + { ContentType.Mseq, "application/vnd.mseq" }, + { ContentType.Msf, "application/vnd.epson.msf" }, + { ContentType.Msg, "application/vnd.ms-outlook" }, + { ContentType.Msh, "model/mesh" }, + { ContentType.Msi, "application/octet-stream" }, + { ContentType.Msl, "application/vnd.mobius.msl" }, + { ContentType.Msm, "application/octet-stream" }, + { ContentType.Msp, "application/octet-stream" }, + { ContentType.Msty, "application/vnd.muvee.style" }, + { ContentType.Mtl, "model/mtl" }, + { ContentType.Mts, "model/vnd.mts" }, + { ContentType.Mus, "application/vnd.musician" }, + { ContentType.Musd, "application/mmt-usd+xml" }, + { ContentType.Musicxml, "application/vnd.recordare.musicxml+xml" }, + { ContentType.Mvb, "application/x-msmediaview" }, + { ContentType.Mvt, "application/vnd.mapbox-vector-tile" }, + { ContentType.Mwf, "application/vnd.mfer" }, + { ContentType.Mxf, "application/mxf" }, + { ContentType.Mxl, "application/vnd.recordare.musicxml" }, + { ContentType.Mxmf, "audio/mobile-xmf" }, + { ContentType.Mxml, "application/xv+xml" }, + { ContentType.Mxs, "application/vnd.triscape.mxs" }, + { ContentType.Mxu, "video/vnd.mpegurl" }, + { ContentType.N3, "text/n3" }, + { ContentType.Nb, "application/mathematica" }, + { ContentType.Nbp, "application/vnd.wolfram.player" }, + { ContentType.Nc, "application/x-netcdf" }, + { ContentType.Ncx, "application/x-dtbncx+xml" }, + { ContentType.Nfo, "text/x-nfo" }, + { ContentType.Ngdat, "application/vnd.nokia.n-gage.data" }, + { ContentType.Nitf, "application/vnd.nitf" }, + { ContentType.Nlu, "application/vnd.neurolanguage.nlu" }, + { ContentType.Nml, "application/vnd.enliven" }, + { ContentType.Nnd, "application/vnd.noblenet-directory" }, + { ContentType.Nns, "application/vnd.noblenet-sealer" }, + { ContentType.Nnw, "application/vnd.noblenet-web" }, + { ContentType.Npx, "image/vnd.net-fpx" }, + { ContentType.Nq, "application/n-quads" }, + { ContentType.Nsc, "application/x-conference" }, + { ContentType.Nsf, "application/vnd.lotus-notes" }, + { ContentType.Nt, "application/n-triples" }, + { ContentType.Ntf, "application/vnd.nitf" }, + { ContentType.Numbers, "application/vnd.apple.numbers" }, + { ContentType.Nzb, "application/x-nzb" }, + { ContentType.Oa2, "application/vnd.fujitsu.oasys2" }, + { ContentType.Oa3, "application/vnd.fujitsu.oasys3" }, + { ContentType.Oas, "application/vnd.fujitsu.oasys" }, + { ContentType.Obd, "application/x-msbinder" }, + { ContentType.Obgx, "application/vnd.openblox.game+xml" }, + { ContentType.Obj, "application/x-tgif" }, + { ContentType.Oda, "application/oda" }, + { ContentType.Odb, "application/vnd.oasis.opendocument.database" }, + { ContentType.Odc, "application/vnd.oasis.opendocument.chart" }, + { ContentType.Odf, "application/vnd.oasis.opendocument.formula" }, + { ContentType.Odft, "application/vnd.oasis.opendocument.formula-template" }, + { ContentType.Odg, "application/vnd.oasis.opendocument.graphics" }, + { ContentType.Odi, "application/vnd.oasis.opendocument.image" }, + { ContentType.Odm, "application/vnd.oasis.opendocument.text-master" }, + { ContentType.Odp, "application/vnd.oasis.opendocument.presentation" }, + { ContentType.Ods, "application/vnd.oasis.opendocument.spreadsheet" }, + { ContentType.Odt, "application/vnd.oasis.opendocument.text" }, + { ContentType.Oga, "audio/ogg" }, + { ContentType.Ogex, "model/vnd.opengex" }, + { ContentType.Ogg, "audio/ogg" }, + { ContentType.Ogv, "video/ogg" }, + { ContentType.Ogx, "application/ogg" }, + { ContentType.Omdoc, "application/omdoc+xml" }, + { ContentType.Onepkg, "application/onenote" }, + { ContentType.Onetmp, "application/onenote" }, + { ContentType.Onetoc, "application/onenote" }, + { ContentType.Onetoc2, "application/onenote" }, + { ContentType.Opf, "application/oebps-package+xml" }, + { ContentType.Opml, "text/x-opml" }, + { ContentType.Oprc, "application/vnd.palm" }, + { ContentType.Opus, "audio/ogg" }, + { ContentType.Org, "application/vnd.lotus-organizer" }, + { ContentType.Osf, "application/vnd.yamaha.openscoreformat" }, + { ContentType.Osfpvg, "application/vnd.yamaha.openscoreformat.osfpvg+xml" }, + { ContentType.Osm, "application/vnd.openstreetmap.data+xml" }, + { ContentType.Otc, "application/vnd.oasis.opendocument.chart-template" }, + { ContentType.Otf, "font/otf" }, + { ContentType.Otg, "application/vnd.oasis.opendocument.graphics-template" }, + { ContentType.Oth, "application/vnd.oasis.opendocument.text-web" }, + { ContentType.Oti, "application/vnd.oasis.opendocument.image-template" }, + { ContentType.Otp, "application/vnd.oasis.opendocument.presentation-template" }, + { ContentType.Ots, "application/vnd.oasis.opendocument.spreadsheet-template" }, + { ContentType.Ott, "application/vnd.oasis.opendocument.text-template" }, + { ContentType.Ova, "application/x-virtualbox-ova" }, + { ContentType.Ovf, "application/x-virtualbox-ovf" }, + { ContentType.Owl, "application/rdf+xml" }, + { ContentType.Oxps, "application/oxps" }, + { ContentType.Oxt, "application/vnd.openofficeorg.extension" }, + { ContentType.P, "text/x-pascal" }, + { ContentType.P10, "application/pkcs10" }, + { ContentType.P12, "application/x-pkcs12" }, + { ContentType.P7b, "application/x-pkcs7-certificates" }, + { ContentType.P7c, "application/pkcs7-mime" }, + { ContentType.P7m, "application/pkcs7-mime" }, + { ContentType.P7r, "application/x-pkcs7-certreqresp" }, + { ContentType.P7s, "application/pkcs7-signature" }, + { ContentType.P8, "application/pkcs8" }, + { ContentType.Pac, "application/x-ns-proxy-autoconfig" }, + { ContentType.Pages, "application/vnd.apple.pages" }, + { ContentType.Pas, "text/x-pascal" }, + { ContentType.Paw, "application/vnd.pawaafile" }, + { ContentType.Pbd, "application/vnd.powerbuilder6" }, + { ContentType.Pbm, "image/x-portable-bitmap" }, + { ContentType.Pcap, "application/vnd.tcpdump.pcap" }, + { ContentType.Pcf, "application/x-font-pcf" }, + { ContentType.Pcl, "application/vnd.hp-pcl" }, + { ContentType.Pclxl, "application/vnd.hp-pclxl" }, + { ContentType.Pct, "image/x-pict" }, + { ContentType.Pcurl, "application/vnd.curl.pcurl" }, + { ContentType.Pcx, "image/vnd.zbrush.pcx" }, + { ContentType.Pdb, "application/vnd.palm" }, + { ContentType.Pde, "text/x-processing" }, + { ContentType.Pdf, "application/pdf" }, + { ContentType.Pem, "application/x-x509-ca-cert" }, + { ContentType.Pfa, "application/x-font-type1" }, + { ContentType.Pfb, "application/x-font-type1" }, + { ContentType.Pfm, "application/x-font-type1" }, + { ContentType.Pfr, "application/font-tdpfr" }, + { ContentType.Pfx, "application/x-pkcs12" }, + { ContentType.Pgm, "image/x-portable-graymap" }, + { ContentType.Pgn, "application/x-chess-pgn" }, + { ContentType.Pgp, "application/pgp-encrypted" }, + { ContentType.Php, "application/x-httpd-php" }, + { ContentType.Pic, "image/x-pict" }, + { ContentType.Pkg, "application/octet-stream" }, + { ContentType.Pki, "application/pkixcmp" }, + { ContentType.Pkipath, "application/pkix-pkipath" }, + { ContentType.Pkpass, "application/vnd.apple.pkpass" }, + { ContentType.Pl, "application/x-perl" }, + { ContentType.Plb, "application/vnd.3gpp.pic-bw-large" }, + { ContentType.Plc, "application/vnd.mobius.plc" }, + { ContentType.Plf, "application/vnd.pocketlearn" }, + { ContentType.Pls, "application/pls+xml" }, + { ContentType.Pm, "application/x-perl" }, + { ContentType.Pml, "application/vnd.ctc-posml" }, + { ContentType.Png, "image/png" }, + { ContentType.Pnm, "image/x-portable-anymap" }, + { ContentType.Portpkg, "application/vnd.macports.portpkg" }, + { ContentType.Pot, "application/vnd.ms-powerpoint" }, + { ContentType.Potm, "application/vnd.ms-powerpoint.template.macroenabled.12" }, + { ContentType.Potx, "application/vnd.openxmlformats-officedocument.presentationml.template" }, + { ContentType.Ppam, "application/vnd.ms-powerpoint.addin.macroenabled.12" }, + { ContentType.Ppd, "application/vnd.cups-ppd" }, + { ContentType.Ppm, "image/x-portable-pixmap" }, + { ContentType.Pps, "application/vnd.ms-powerpoint" }, + { ContentType.Ppsm, "application/vnd.ms-powerpoint.slideshow.macroenabled.12" }, + { ContentType.Ppsx, "application/vnd.openxmlformats-officedocument.presentationml.slideshow" }, + { ContentType.Ppt, "application/vnd.ms-powerpoint" }, + { ContentType.Pptm, "application/vnd.ms-powerpoint.presentation.macroenabled.12" }, + { ContentType.Pptx, "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { ContentType.Pqa, "application/vnd.palm" }, + { ContentType.Prc, "application/x-mobipocket-ebook" }, + { ContentType.Pre, "application/vnd.lotus-freelance" }, + { ContentType.Prf, "application/pics-rules" }, + { ContentType.Provx, "application/provenance+xml" }, + { ContentType.Ps, "application/postscript" }, + { ContentType.Psb, "application/vnd.3gpp.pic-bw-small" }, + { ContentType.Psd, "image/vnd.adobe.photoshop" }, + { ContentType.Psf, "application/x-font-linux-psf" }, + { ContentType.Pskcxml, "application/pskc+xml" }, + { ContentType.Pti, "image/prs.pti" }, + { ContentType.Ptid, "application/vnd.pvi.ptid1" }, + { ContentType.Pub, "application/x-mspublisher" }, + { ContentType.Pvb, "application/vnd.3gpp.pic-bw-var" }, + { ContentType.Pwn, "application/vnd.3m.post-it-notes" }, + { ContentType.Pya, "audio/vnd.ms-playready.media.pya" }, + { ContentType.Pyv, "video/vnd.ms-playready.media.pyv" }, + { ContentType.Qam, "application/vnd.epson.quickanime" }, + { ContentType.Qbo, "application/vnd.intu.qbo" }, + { ContentType.Qfx, "application/vnd.intu.qfx" }, + { ContentType.Qps, "application/vnd.publishare-delta-tree" }, + { ContentType.Qt, "video/quicktime" }, + { ContentType.Qwd, "application/vnd.quark.quarkxpress" }, + { ContentType.Qwt, "application/vnd.quark.quarkxpress" }, + { ContentType.Qxb, "application/vnd.quark.quarkxpress" }, + { ContentType.Qxd, "application/vnd.quark.quarkxpress" }, + { ContentType.Qxl, "application/vnd.quark.quarkxpress" }, + { ContentType.Qxt, "application/vnd.quark.quarkxpress" }, + { ContentType.Ra, "audio/x-pn-realaudio" }, + { ContentType.Ram, "audio/x-pn-realaudio" }, + { ContentType.Raml, "application/raml+yaml" }, + { ContentType.Rapd, "application/route-apd+xml" }, + { ContentType.Rar, "application/vnd.rar" }, + { ContentType.Ras, "image/x-cmu-raster" }, + { ContentType.Rdf, "application/rdf+xml" }, + { ContentType.Rdz, "application/vnd.data-vision.rdz" }, + { ContentType.Relo, "application/p2p-overlay+xml" }, + { ContentType.Rep, "application/vnd.businessobjects" }, + { ContentType.Res, "application/x-dtbresource+xml" }, + { ContentType.Rgb, "image/x-rgb" }, + { ContentType.Rif, "application/reginfo+xml" }, + { ContentType.Rip, "audio/vnd.rip" }, + { ContentType.Ris, "application/x-research-info-systems" }, + { ContentType.Rl, "application/resource-lists+xml" }, + { ContentType.Rlc, "image/vnd.fujixerox.edmics-rlc" }, + { ContentType.Rld, "application/resource-lists-diff+xml" }, + { ContentType.Rm, "application/vnd.rn-realmedia" }, + { ContentType.Rmi, "audio/midi" }, + { ContentType.Rmp, "audio/x-pn-realaudio-plugin" }, + { ContentType.Rms, "application/vnd.jcp.javame.midlet-rms" }, + { ContentType.Rmvb, "application/vnd.rn-realmedia-vbr" }, + { ContentType.Rnc, "application/relax-ng-compact-syntax" }, + { ContentType.Rng, "application/xml" }, + { ContentType.Roa, "application/rpki-roa" }, + { ContentType.Roff, "text/troff" }, + { ContentType.Rp9, "application/vnd.cloanto.rp9" }, + { ContentType.Rpm, "application/x-redhat-package-manager" }, + { ContentType.Rpss, "application/vnd.nokia.radio-presets" }, + { ContentType.Rpst, "application/vnd.nokia.radio-preset" }, + { ContentType.Rq, "application/sparql-query" }, + { ContentType.Rs, "application/rls-services+xml" }, + { ContentType.Rsat, "application/atsc-rsat+xml" }, + { ContentType.Rsd, "application/rsd+xml" }, + { ContentType.Rsheet, "application/urc-ressheet+xml" }, + { ContentType.Rss, "application/rss+xml" }, + { ContentType.Rtf, "application/rtf" }, + { ContentType.Rtx, "text/richtext" }, + { ContentType.Run, "application/x-makeself" }, + { ContentType.Rusd, "application/route-usd+xml" }, + { ContentType.S, "text/x-asm" }, + { ContentType.S3m, "audio/s3m" }, + { ContentType.Saf, "application/vnd.yamaha.smaf-audio" }, + { ContentType.Sass, "text/x-sass" }, + { ContentType.Sbml, "application/sbml+xml" }, + { ContentType.Sc, "application/vnd.ibm.secure-container" }, + { ContentType.Scd, "application/x-msschedule" }, + { ContentType.Scm, "application/vnd.lotus-screencam" }, + { ContentType.Scq, "application/scvp-cv-request" }, + { ContentType.Scs, "application/scvp-cv-response" }, + { ContentType.Scss, "text/x-scss" }, + { ContentType.Scurl, "text/vnd.curl.scurl" }, + { ContentType.Sda, "application/vnd.stardivision.draw" }, + { ContentType.Sdc, "application/vnd.stardivision.calc" }, + { ContentType.Sdd, "application/vnd.stardivision.impress" }, + { ContentType.Sdkd, "application/vnd.solent.sdkm+xml" }, + { ContentType.Sdkm, "application/vnd.solent.sdkm+xml" }, + { ContentType.Sdp, "application/sdp" }, + { ContentType.Sdw, "application/vnd.stardivision.writer" }, + { ContentType.Sea, "application/x-sea" }, + { ContentType.See, "application/vnd.seemail" }, + { ContentType.Seed, "application/vnd.fdsn.seed" }, + { ContentType.Sema, "application/vnd.sema" }, + { ContentType.Semd, "application/vnd.semd" }, + { ContentType.Semf, "application/vnd.semf" }, + { ContentType.Senmlx, "application/senml+xml" }, + { ContentType.Sensmlx, "application/sensml+xml" }, + { ContentType.Ser, "application/java-serialized-object" }, + { ContentType.Setpay, "application/set-payment-initiation" }, + { ContentType.Setreg, "application/set-registration-initiation" }, + { ContentType.Sfs, "application/vnd.spotfire.sfs" }, + { ContentType.Sfv, "text/x-sfv" }, + { ContentType.Sgi, "image/sgi" }, + { ContentType.Sgl, "application/vnd.stardivision.writer-global" }, + { ContentType.Sgm, "text/sgml" }, + { ContentType.Sgml, "text/sgml" }, + { ContentType.Sh, "application/x-sh" }, + { ContentType.Shar, "application/x-shar" }, + { ContentType.Shex, "text/shex" }, + { ContentType.Shf, "application/shf+xml" }, + { ContentType.Shtml, "text/html" }, + { ContentType.Sid, "image/x-mrsid-image" }, + { ContentType.Sieve, "application/sieve" }, + { ContentType.Sig, "application/pgp-signature" }, + { ContentType.Sil, "audio/silk" }, + { ContentType.Silo, "model/mesh" }, + { ContentType.Sis, "application/vnd.symbian.install" }, + { ContentType.Sisx, "application/vnd.symbian.install" }, + { ContentType.Sit, "application/x-stuffit" }, + { ContentType.Sitx, "application/x-stuffitx" }, + { ContentType.Siv, "application/sieve" }, + { ContentType.Skd, "application/vnd.koan" }, + { ContentType.Skm, "application/vnd.koan" }, + { ContentType.Skp, "application/vnd.koan" }, + { ContentType.Skt, "application/vnd.koan" }, + { ContentType.Sldm, "application/vnd.ms-powerpoint.slide.macroenabled.12" }, + { ContentType.Sldx, "application/vnd.openxmlformats-officedocument.presentationml.slide" }, + { ContentType.Slim, "text/slim" }, + { ContentType.Slm, "text/slim" }, + { ContentType.Sls, "application/route-s-tsid+xml" }, + { ContentType.Slt, "application/vnd.epson.salt" }, + { ContentType.Sm, "application/vnd.stepmania.stepchart" }, + { ContentType.Smf, "application/vnd.stardivision.math" }, + { ContentType.Smi, "application/smil+xml" }, + { ContentType.Smil, "application/smil+xml" }, + { ContentType.Smv, "video/x-smv" }, + { ContentType.Smzip, "application/vnd.stepmania.package" }, + { ContentType.Snd, "audio/basic" }, + { ContentType.Snf, "application/x-font-snf" }, + { ContentType.So, "application/octet-stream" }, + { ContentType.Spc, "application/x-pkcs7-certificates" }, + { ContentType.Spdx, "text/spdx" }, + { ContentType.Spf, "application/vnd.yamaha.smaf-phrase" }, + { ContentType.Spl, "application/x-futuresplash" }, + { ContentType.Spot, "text/vnd.in3d.spot" }, + { ContentType.Spp, "application/scvp-vp-response" }, + { ContentType.Spq, "application/scvp-vp-request" }, + { ContentType.Spx, "audio/ogg" }, + { ContentType.Sql, "application/x-sql" }, + { ContentType.Src, "application/x-wais-source" }, + { ContentType.Srt, "application/x-subrip" }, + { ContentType.Sru, "application/sru+xml" }, + { ContentType.Srx, "application/sparql-results+xml" }, + { ContentType.Ssdl, "application/ssdl+xml" }, + { ContentType.Sse, "application/vnd.kodak-descriptor" }, + { ContentType.Ssf, "application/vnd.epson.ssf" }, + { ContentType.Ssml, "application/ssml+xml" }, + { ContentType.St, "application/vnd.sailingtracker.track" }, + { ContentType.Stc, "application/vnd.sun.xml.calc.template" }, + { ContentType.Std, "application/vnd.sun.xml.draw.template" }, + { ContentType.Stf, "application/vnd.wt.stf" }, + { ContentType.Sti, "application/vnd.sun.xml.impress.template" }, + { ContentType.Stk, "application/hyperstudio" }, + { ContentType.Stl, "application/vnd.ms-pki.stl" }, + { ContentType.Stpx, "model/step+xml" }, + { ContentType.Stpxz, "model/step-xml+zip" }, + { ContentType.Stpz, "model/step+zip" }, + { ContentType.Str, "application/vnd.pg.format" }, + { ContentType.Stw, "application/vnd.sun.xml.writer.template" }, + { ContentType.Styl, "text/stylus" }, + { ContentType.Stylus, "text/stylus" }, + { ContentType.Sub, "image/vnd.dvb.subtitle" }, + { ContentType.Sus, "application/vnd.sus-calendar" }, + { ContentType.Susp, "application/vnd.sus-calendar" }, + { ContentType.Sv4cpio, "application/x-sv4cpio" }, + { ContentType.Sv4crc, "application/x-sv4crc" }, + { ContentType.Svc, "application/vnd.dvb.service" }, + { ContentType.Svd, "application/vnd.svd" }, + { ContentType.Svg, "image/svg+xml" }, + { ContentType.Svgz, "image/svg+xml" }, + { ContentType.Swa, "application/x-director" }, + { ContentType.Swf, "application/x-shockwave-flash" }, + { ContentType.Swi, "application/vnd.aristanetworks.swi" }, + { ContentType.Swidtag, "application/swid+xml" }, + { ContentType.Sxc, "application/vnd.sun.xml.calc" }, + { ContentType.Sxd, "application/vnd.sun.xml.draw" }, + { ContentType.Sxg, "application/vnd.sun.xml.writer.global" }, + { ContentType.Sxi, "application/vnd.sun.xml.impress" }, + { ContentType.Sxm, "application/vnd.sun.xml.math" }, + { ContentType.Sxw, "application/vnd.sun.xml.writer" }, + { ContentType.T, "text/troff" }, + { ContentType.T3, "application/x-t3vm-image" }, + { ContentType.T38, "image/t38" }, + { ContentType.Taglet, "application/vnd.mynfc" }, + { ContentType.Tao, "application/vnd.tao.intent-module-archive" }, + { ContentType.Tap, "image/vnd.tencent.tap" }, + { ContentType.Tar, "application/x-tar" }, + { ContentType.Tcap, "application/vnd.3gpp2.tcap" }, + { ContentType.Tcl, "application/x-tcl" }, + { ContentType.Td, "application/urc-targetdesc+xml" }, + { ContentType.Teacher, "application/vnd.smart.teacher" }, + { ContentType.Tei, "application/tei+xml" }, + { ContentType.Tex, "application/x-tex" }, + { ContentType.Texi, "application/x-texinfo" }, + { ContentType.Texinfo, "application/x-texinfo" }, + { ContentType.Text, "text/plain" }, + { ContentType.Tfi, "application/thraud+xml" }, + { ContentType.Tfm, "application/x-tex-tfm" }, + { ContentType.Tfx, "image/tiff-fx" }, + { ContentType.Tga, "image/x-tga" }, + { ContentType.Thmx, "application/vnd.ms-officetheme" }, + { ContentType.Tif, "image/tiff" }, + { ContentType.Tiff, "image/tiff" }, + { ContentType.Tk, "application/x-tcl" }, + { ContentType.Tmo, "application/vnd.tmobile-livetv" }, + { ContentType.Toml, "application/toml" }, + { ContentType.Torrent, "application/x-bittorrent" }, + { ContentType.Tpl, "application/vnd.groove-tool-template" }, + { ContentType.Tpt, "application/vnd.trid.tpt" }, + { ContentType.Tr, "text/troff" }, + { ContentType.Tra, "application/vnd.trueapp" }, + { ContentType.Trig, "application/trig" }, + { ContentType.Trm, "application/x-msterminal" }, + { ContentType.Ts, "video/mp2t" }, + { ContentType.Tsd, "application/timestamped-data" }, + { ContentType.Tsv, "text/tab-separated-values" }, + { ContentType.Ttc, "font/collection" }, + { ContentType.Ttf, "font/ttf" }, + { ContentType.Ttl, "text/turtle" }, + { ContentType.Ttml, "application/ttml+xml" }, + { ContentType.Twd, "application/vnd.simtech-mindmapper" }, + { ContentType.Twds, "application/vnd.simtech-mindmapper" }, + { ContentType.Txd, "application/vnd.genomatix.tuxedo" }, + { ContentType.Txf, "application/vnd.mobius.txf" }, + { ContentType.Txt, "text/plain" }, + { ContentType.U32, "application/x-authorware-bin" }, + { ContentType.U8dsn, "message/global-delivery-status" }, + { ContentType.U8hdr, "message/global-headers" }, + { ContentType.U8mdn, "message/global-disposition-notification" }, + { ContentType.U8msg, "message/global" }, + { ContentType.Ubj, "application/ubjson" }, + { ContentType.Udeb, "application/x-debian-package" }, + { ContentType.Ufd, "application/vnd.ufdl" }, + { ContentType.Ufdl, "application/vnd.ufdl" }, + { ContentType.Ulx, "application/x-glulx" }, + { ContentType.Umj, "application/vnd.umajin" }, + { ContentType.Unityweb, "application/vnd.unity" }, + { ContentType.Uoml, "application/vnd.uoml+xml" }, + { ContentType.Uri, "text/uri-list" }, + { ContentType.Uris, "text/uri-list" }, + { ContentType.Urls, "text/uri-list" }, + { ContentType.Usdz, "model/vnd.usdz+zip" }, + { ContentType.Ustar, "application/x-ustar" }, + { ContentType.Utz, "application/vnd.uiq.theme" }, + { ContentType.Uu, "text/x-uuencode" }, + { ContentType.Uva, "audio/vnd.dece.audio" }, + { ContentType.Uvd, "application/vnd.dece.data" }, + { ContentType.Uvf, "application/vnd.dece.data" }, + { ContentType.Uvg, "image/vnd.dece.graphic" }, + { ContentType.Uvh, "video/vnd.dece.hd" }, + { ContentType.Uvi, "image/vnd.dece.graphic" }, + { ContentType.Uvm, "video/vnd.dece.mobile" }, + { ContentType.Uvp, "video/vnd.dece.pd" }, + { ContentType.Uvs, "video/vnd.dece.sd" }, + { ContentType.Uvt, "application/vnd.dece.ttml+xml" }, + { ContentType.Uvu, "video/vnd.uvvu.mp4" }, + { ContentType.Uvv, "video/vnd.dece.video" }, + { ContentType.Uvva, "audio/vnd.dece.audio" }, + { ContentType.Uvvd, "application/vnd.dece.data" }, + { ContentType.Uvvf, "application/vnd.dece.data" }, + { ContentType.Uvvg, "image/vnd.dece.graphic" }, + { ContentType.Uvvh, "video/vnd.dece.hd" }, + { ContentType.Uvvi, "image/vnd.dece.graphic" }, + { ContentType.Uvvm, "video/vnd.dece.mobile" }, + { ContentType.Uvvp, "video/vnd.dece.pd" }, + { ContentType.Uvvs, "video/vnd.dece.sd" }, + { ContentType.Uvvt, "application/vnd.dece.ttml+xml" }, + { ContentType.Uvvu, "video/vnd.uvvu.mp4" }, + { ContentType.Uvvv, "video/vnd.dece.video" }, + { ContentType.Uvvx, "application/vnd.dece.unspecified" }, + { ContentType.Uvvz, "application/vnd.dece.zip" }, + { ContentType.Uvx, "application/vnd.dece.unspecified" }, + { ContentType.Uvz, "application/vnd.dece.zip" }, + { ContentType.Vbox, "application/x-virtualbox-vbox" }, + { ContentType.Vcard, "text/vcard" }, + { ContentType.Vcd, "application/x-cdlink" }, + { ContentType.Vcf, "text/x-vcard" }, + { ContentType.Vcg, "application/vnd.groove-vcard" }, + { ContentType.Vcs, "text/x-vcalendar" }, + { ContentType.Vcx, "application/vnd.vcx" }, + { ContentType.Vdi, "application/x-virtualbox-vdi" }, + { ContentType.Vds, "model/vnd.sap.vds" }, + { ContentType.Vhd, "application/x-virtualbox-vhd" }, + { ContentType.Vis, "application/vnd.visionary" }, + { ContentType.Viv, "video/vnd.vivo" }, + { ContentType.Vmdk, "application/x-virtualbox-vmdk" }, + { ContentType.Vob, "video/x-ms-vob" }, + { ContentType.Vor, "application/vnd.stardivision.writer" }, + { ContentType.Vox, "application/x-authorware-bin" }, + { ContentType.Vrml, "model/vrml" }, + { ContentType.Vsd, "application/vnd.visio" }, + { ContentType.Vsf, "application/vnd.vsf" }, + { ContentType.Vss, "application/vnd.visio" }, + { ContentType.Vst, "application/vnd.visio" }, + { ContentType.Vsw, "application/vnd.visio" }, + { ContentType.Vtf, "image/vnd.valve.source.texture" }, + { ContentType.Vtt, "text/vtt" }, + { ContentType.Vtu, "model/vnd.vtu" }, + { ContentType.Vxml, "application/voicexml+xml" }, + { ContentType.W3d, "application/x-director" }, + { ContentType.Wad, "application/x-doom" }, + { ContentType.Wadl, "application/vnd.sun.wadl+xml" }, + { ContentType.War, "application/java-archive" }, + { ContentType.Wasm, "application/wasm" }, + { ContentType.Wav, "audio/wav" }, + { ContentType.Wax, "audio/x-ms-wax" }, + { ContentType.Wbmp, "image/vnd.wap.wbmp" }, + { ContentType.Wbs, "application/vnd.criticaltools.wbs+xml" }, + { ContentType.Wbxml, "application/vnd.wap.wbxml" }, + { ContentType.Wcm, "application/vnd.ms-works" }, + { ContentType.Wdb, "application/vnd.ms-works" }, + { ContentType.Wdp, "image/vnd.ms-photo" }, + { ContentType.Weba, "audio/webm" }, + { ContentType.Webapp, "application/x-web-app-manifest+json" }, + { ContentType.Webm, "video/webm" }, + { ContentType.Webp, "image/webp" }, + { ContentType.Wg, "application/vnd.pmi.widget" }, + { ContentType.Wgt, "application/widget" }, + { ContentType.Wks, "application/vnd.ms-works" }, + { ContentType.Wm, "video/x-ms-wm" }, + { ContentType.Wma, "audio/x-ms-wma" }, + { ContentType.Wmd, "application/x-ms-wmd" }, + { ContentType.Wmf, "application/x-msmetafile" }, + { ContentType.Wml, "text/vnd.wap.wml" }, + { ContentType.Wmlc, "application/vnd.wap.wmlc" }, + { ContentType.Wmls, "text/vnd.wap.wmlscript" }, + { ContentType.Wmlsc, "application/vnd.wap.wmlscriptc" }, + { ContentType.Wmv, "video/x-ms-wmv" }, + { ContentType.Wmx, "video/x-ms-wmx" }, + { ContentType.Wmz, "application/x-ms-wmz" }, + { ContentType.Woff, "font/woff" }, + { ContentType.Woff2, "font/woff2" }, + { ContentType.Wpd, "application/vnd.wordperfect" }, + { ContentType.Wpl, "application/vnd.ms-wpl" }, + { ContentType.Wps, "application/vnd.ms-works" }, + { ContentType.Wqd, "application/vnd.wqd" }, + { ContentType.Wri, "application/x-mswrite" }, + { ContentType.Wrl, "model/vrml" }, + { ContentType.Wsc, "message/vnd.wfa.wsc" }, + { ContentType.Wsdl, "application/wsdl+xml" }, + { ContentType.Wspolicy, "application/wspolicy+xml" }, + { ContentType.Wtb, "application/vnd.webturbo" }, + { ContentType.Wvx, "video/x-ms-wvx" }, + { ContentType.X32, "application/x-authorware-bin" }, + { ContentType.X3d, "model/x3d+xml" }, + { ContentType.X3db, "model/x3d+binary" }, + { ContentType.X3dbz, "model/x3d+binary" }, + { ContentType.X3dv, "model/x3d+vrml" }, + { ContentType.X3dvz, "model/x3d+vrml" }, + { ContentType.X3dz, "model/x3d+xml" }, + { ContentType.Xaml, "application/xaml+xml" }, + { ContentType.Xap, "application/x-silverlight-app" }, + { ContentType.Xar, "application/vnd.xara" }, + { ContentType.Xav, "application/xcap-att+xml" }, + { ContentType.Xbap, "application/x-ms-xbap" }, + { ContentType.Xbd, "application/vnd.fujixerox.docuworks.binder" }, + { ContentType.Xbm, "image/x-xbitmap" }, + { ContentType.Xca, "application/xcap-caps+xml" }, + { ContentType.Xcs, "application/calendar+xml" }, + { ContentType.Xdf, "application/xcap-diff+xml" }, + { ContentType.Xdm, "application/vnd.syncml.dm+xml" }, + { ContentType.Xdp, "application/vnd.adobe.xdp+xml" }, + { ContentType.Xdssc, "application/dssc+xml" }, + { ContentType.Xdw, "application/vnd.fujixerox.docuworks" }, + { ContentType.Xel, "application/xcap-el+xml" }, + { ContentType.Xenc, "application/xenc+xml" }, + { ContentType.Xer, "application/patch-ops-error+xml" }, + { ContentType.Xfdf, "application/vnd.adobe.xfdf" }, + { ContentType.Xfdl, "application/vnd.xfdl" }, + { ContentType.Xht, "application/xhtml+xml" }, + { ContentType.Xhtml, "application/xhtml+xml" }, + { ContentType.Xhvml, "application/xv+xml" }, + { ContentType.Xif, "image/vnd.xiff" }, + { ContentType.Xla, "application/vnd.ms-excel" }, + { ContentType.Xlam, "application/vnd.ms-excel.addin.macroenabled.12" }, + { ContentType.Xlc, "application/vnd.ms-excel" }, + { ContentType.Xlf, "application/x-xliff+xml" }, + { ContentType.Xlm, "application/vnd.ms-excel" }, + { ContentType.Xls, "application/vnd.ms-excel" }, + { ContentType.Xlsb, "application/vnd.ms-excel.sheet.binary.macroenabled.12" }, + { ContentType.Xlsm, "application/vnd.ms-excel.sheet.macroenabled.12" }, + { ContentType.Xlsx, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { ContentType.Xlt, "application/vnd.ms-excel" }, + { ContentType.Xltm, "application/vnd.ms-excel.template.macroenabled.12" }, + { ContentType.Xltx, "application/vnd.openxmlformats-officedocument.spreadsheetml.template" }, + { ContentType.Xlw, "application/vnd.ms-excel" }, + { ContentType.Xm, "audio/xm" }, + { ContentType.Xml, "application/xml" }, + { ContentType.Xns, "application/xcap-ns+xml" }, + { ContentType.Xo, "application/vnd.olpc-sugar" }, + { ContentType.Xop, "application/xop+xml" }, + { ContentType.Xpi, "application/x-xpinstall" }, + { ContentType.Xpl, "application/xproc+xml" }, + { ContentType.Xpm, "image/x-xpixmap" }, + { ContentType.Xpr, "application/vnd.is-xpr" }, + { ContentType.Xps, "application/vnd.ms-xpsdocument" }, + { ContentType.Xpw, "application/vnd.intercon.formnet" }, + { ContentType.Xpx, "application/vnd.intercon.formnet" }, + { ContentType.Xsd, "application/xml" }, + { ContentType.Xsl, "application/xml" }, + { ContentType.Xslt, "application/xslt+xml" }, + { ContentType.Xsm, "application/vnd.syncml+xml" }, + { ContentType.Xspf, "application/xspf+xml" }, + { ContentType.Xul, "application/vnd.mozilla.xul+xml" }, + { ContentType.Xvm, "application/xv+xml" }, + { ContentType.Xvml, "application/xv+xml" }, + { ContentType.Xwd, "image/x-xwindowdump" }, + { ContentType.Xyz, "chemical/x-xyz" }, + { ContentType.Xz, "application/x-xz" }, + { ContentType.Yaml, "text/yaml" }, + { ContentType.Yang, "application/yang" }, + { ContentType.Yin, "application/yin+xml" }, + { ContentType.Yml, "text/yaml" }, + { ContentType.Ymp, "text/x-suse-ymp" }, + { ContentType.Z1, "application/x-zmachine" }, + { ContentType.Z2, "application/x-zmachine" }, + { ContentType.Z3, "application/x-zmachine" }, + { ContentType.Z4, "application/x-zmachine" }, + { ContentType.Z5, "application/x-zmachine" }, + { ContentType.Z6, "application/x-zmachine" }, + { ContentType.Z7, "application/x-zmachine" }, + { ContentType.Z8, "application/x-zmachine" }, + { ContentType.Zaz, "application/vnd.zzazz.deck+xml" }, + { ContentType.Zip, "application/zip" }, + { ContentType.Zir, "application/vnd.zul" }, + { ContentType.Zirz, "application/vnd.zul" }, + { ContentType.Zmm, "application/vnd.handheld-entertainment+xml" }, + }; + private static readonly IReadOnlyDictionary<string, ContentType> ExtensionToCt = new Dictionary<string, ContentType>() + { + { "aab", ContentType.Aab }, + { "aac", ContentType.Aac }, + { "aam", ContentType.Aam }, + { "aas", ContentType.Aas }, + { "abw", ContentType.Abw }, + { "ac", ContentType.Ac }, + { "acc", ContentType.Acc }, + { "ace", ContentType.Ace }, + { "acu", ContentType.Acu }, + { "acutc", ContentType.Acutc }, + { "adp", ContentType.Adp }, + { "aep", ContentType.Aep }, + { "afm", ContentType.Afm }, + { "afp", ContentType.Afp }, + { "ahead", ContentType.Ahead }, + { "ai", ContentType.Ai }, + { "aif", ContentType.Aif }, + { "aifc", ContentType.Aifc }, + { "aiff", ContentType.Aiff }, + { "air", ContentType.Air }, + { "ait", ContentType.Ait }, + { "ami", ContentType.Ami }, + { "amr", ContentType.Amr }, + { "apk", ContentType.Apk }, + { "apng", ContentType.Apng }, + { "appcache", ContentType.Appcache }, + { "apr", ContentType.Apr }, + { "arc", ContentType.Arc }, + { "arj", ContentType.Arj }, + { "asc", ContentType.Asc }, + { "asf", ContentType.Asf }, + { "asm", ContentType.Asm }, + { "aso", ContentType.Aso }, + { "asx", ContentType.Asx }, + { "atc", ContentType.Atc }, + { "atom", ContentType.Atom }, + { "atomcat", ContentType.Atomcat }, + { "atomsvc", ContentType.Atomsvc }, + { "atx", ContentType.Atx }, + { "au", ContentType.Au }, + { "avi", ContentType.Avi }, + { "avif", ContentType.Avif }, + { "aw", ContentType.Aw }, + { "azf", ContentType.Azf }, + { "azs", ContentType.Azs }, + { "azv", ContentType.Azv }, + { "azw", ContentType.Azw }, + { "b16", ContentType.B16 }, + { "bat", ContentType.Bat }, + { "bcpio", ContentType.Bcpio }, + { "bdf", ContentType.Bdf }, + { "bdm", ContentType.Bdm }, + { "bdoc", ContentType.Bdoc }, + { "bed", ContentType.Bed }, + { "bh2", ContentType.Bh2 }, + { "bin", ContentType.Binary }, + { "blb", ContentType.Blb }, + { "blorb", ContentType.Blorb }, + { "bmi", ContentType.Bmi }, + { "bmml", ContentType.Bmml }, + { "bmp", ContentType.Bmp }, + { "book", ContentType.Book }, + { "box", ContentType.Box }, + { "boz", ContentType.Boz }, + { "bpk", ContentType.Bpk }, + { "bsp", ContentType.Bsp }, + { "btif", ContentType.Btif }, + { "buffer", ContentType.Buffer }, + { "bz", ContentType.Bz }, + { "bz2", ContentType.Bz2 }, + { "c", ContentType.C }, + { "c11amc", ContentType.C11amc }, + { "c11amz", ContentType.C11amz }, + { "c4d", ContentType.C4d }, + { "c4f", ContentType.C4f }, + { "c4g", ContentType.C4g }, + { "c4p", ContentType.C4p }, + { "c4u", ContentType.C4u }, + { "cab", ContentType.Cab }, + { "caf", ContentType.Caf }, + { "cap", ContentType.Cap }, + { "car", ContentType.Car }, + { "cat", ContentType.Cat }, + { "cb7", ContentType.Cb7 }, + { "cba", ContentType.Cba }, + { "cbr", ContentType.Cbr }, + { "cbt", ContentType.Cbt }, + { "cbz", ContentType.Cbz }, + { "cc", ContentType.Cc }, + { "cco", ContentType.Cco }, + { "cct", ContentType.Cct }, + { "ccxml", ContentType.Ccxml }, + { "cdbcmsg", ContentType.Cdbcmsg }, + { "cdf", ContentType.Cdf }, + { "cdfx", ContentType.Cdfx }, + { "cdkey", ContentType.Cdkey }, + { "cdmia", ContentType.Cdmia }, + { "cdmic", ContentType.Cdmic }, + { "cdmid", ContentType.Cdmid }, + { "cdmio", ContentType.Cdmio }, + { "cdmiq", ContentType.Cdmiq }, + { "cdx", ContentType.Cdx }, + { "cdxml", ContentType.Cdxml }, + { "cdy", ContentType.Cdy }, + { "cer", ContentType.Cer }, + { "cfs", ContentType.Cfs }, + { "cgm", ContentType.Cgm }, + { "chat", ContentType.Chat }, + { "chm", ContentType.Chm }, + { "chrt", ContentType.Chrt }, + { "cif", ContentType.Cif }, + { "cii", ContentType.Cii }, + { "cil", ContentType.Cil }, + { "cjs", ContentType.Cjs }, + { "cla", ContentType.Cla }, + { "clkk", ContentType.Clkk }, + { "clkp", ContentType.Clkp }, + { "clkt", ContentType.Clkt }, + { "clkw", ContentType.Clkw }, + { "clkx", ContentType.Clkx }, + { "clp", ContentType.Clp }, + { "cmc", ContentType.Cmc }, + { "cmdf", ContentType.Cmdf }, + { "cml", ContentType.Cml }, + { "cmp", ContentType.Cmp }, + { "cmx", ContentType.Cmx }, + { "cod", ContentType.Cod }, + { "coffee", ContentType.Coffee }, + { "com", ContentType.Com }, + { "conf", ContentType.Conf }, + { "cpio", ContentType.Cpio }, + { "cpp", ContentType.Cpp }, + { "cpt", ContentType.Cpt }, + { "crd", ContentType.Crd }, + { "crl", ContentType.Crl }, + { "crt", ContentType.Crt }, + { "crx", ContentType.Crx }, + { "csh", ContentType.Csh }, + { "csl", ContentType.Csl }, + { "csml", ContentType.Csml }, + { "csp", ContentType.Csp }, + { "css", ContentType.Css }, + { "cst", ContentType.Cst }, + { "csv", ContentType.Csv }, + { "cu", ContentType.Cu }, + { "curl", ContentType.Curl }, + { "cww", ContentType.Cww }, + { "cxt", ContentType.Cxt }, + { "cxx", ContentType.Cxx }, + { "dae", ContentType.Dae }, + { "daf", ContentType.Daf }, + { "dart", ContentType.Dart }, + { "dataless", ContentType.Dataless }, + { "davmount", ContentType.Davmount }, + { "dbf", ContentType.Dbf }, + { "dbk", ContentType.Dbk }, + { "dcr", ContentType.Dcr }, + { "dcurl", ContentType.Dcurl }, + { "dd2", ContentType.Dd2 }, + { "ddd", ContentType.Ddd }, + { "ddf", ContentType.Ddf }, + { "dds", ContentType.Dds }, + { "deb", ContentType.Deb }, + { "def", ContentType.Def }, + { "deploy", ContentType.Deploy }, + { "der", ContentType.Der }, + { "dfac", ContentType.Dfac }, + { "dgc", ContentType.Dgc }, + { "dic", ContentType.Dic }, + { "dir", ContentType.Dir }, + { "dis", ContentType.Dis }, + { "dist", ContentType.Dist }, + { "distz", ContentType.Distz }, + { "djv", ContentType.Djv }, + { "djvu", ContentType.Djvu }, + { "dll", ContentType.Dll }, + { "dmg", ContentType.Dmg }, + { "dmp", ContentType.Dmp }, + { "dms", ContentType.Dms }, + { "dna", ContentType.Dna }, + { "doc", ContentType.Doc }, + { "docm", ContentType.Docm }, + { "docx", ContentType.Docx }, + { "dot", ContentType.Dot }, + { "dotm", ContentType.Dotm }, + { "dotx", ContentType.Dotx }, + { "dp", ContentType.Dp }, + { "dpg", ContentType.Dpg }, + { "dra", ContentType.Dra }, + { "drle", ContentType.Drle }, + { "dsc", ContentType.Dsc }, + { "dssc", ContentType.Dssc }, + { "dtb", ContentType.Dtb }, + { "dtd", ContentType.Dtd }, + { "dts", ContentType.Dts }, + { "dtshd", ContentType.Dtshd }, + { "dump", ContentType.Dump }, + { "dvb", ContentType.Dvb }, + { "dvi", ContentType.Dvi }, + { "dwd", ContentType.Dwd }, + { "dwf", ContentType.Dwf }, + { "dwg", ContentType.Dwg }, + { "dxf", ContentType.Dxf }, + { "dxp", ContentType.Dxp }, + { "dxr", ContentType.Dxr }, + { "ear", ContentType.Ear }, + { "ecma", ContentType.Ecma }, + { "edm", ContentType.Edm }, + { "edx", ContentType.Edx }, + { "efif", ContentType.Efif }, + { "ei6", ContentType.Ei6 }, + { "elc", ContentType.Elc }, + { "emf", ContentType.Emf }, + { "eml", ContentType.Eml }, + { "emma", ContentType.Emma }, + { "emz", ContentType.Emz }, + { "eol", ContentType.Eol }, + { "eot", ContentType.Eot }, + { "eps", ContentType.Eps }, + { "epub", ContentType.Epub }, + { "es", ContentType.Es }, + { "es3", ContentType.Es3 }, + { "esa", ContentType.Esa }, + { "esf", ContentType.Esf }, + { "et3", ContentType.Et3 }, + { "etx", ContentType.Etx }, + { "eva", ContentType.Eva }, + { "evy", ContentType.Evy }, + { "exe", ContentType.Exe }, + { "exi", ContentType.Exi }, + { "exp", ContentType.Exp }, + { "exr", ContentType.Exr }, + { "ext", ContentType.Ext }, + { "ez", ContentType.Ez }, + { "ez2", ContentType.Ez2 }, + { "ez3", ContentType.Ez3 }, + { "f", ContentType.F }, + { "f4v", ContentType.F4v }, + { "f77", ContentType.Fortran }, + { "f90", ContentType.F90 }, + { "fbs", ContentType.Fbs }, + { "fcdt", ContentType.Fcdt }, + { "fcs", ContentType.Fcs }, + { "fdf", ContentType.Fdf }, + { "fdt", ContentType.Fdt }, + { "fg5", ContentType.Fg5 }, + { "fgd", ContentType.Fgd }, + { "fh", ContentType.Fh }, + { "fh4", ContentType.Fh4 }, + { "fh5", ContentType.Fh5 }, + { "fh7", ContentType.Fh7 }, + { "fhc", ContentType.Fhc }, + { "fig", ContentType.Fig }, + { "fits", ContentType.Fits }, + { "flac", ContentType.Flac }, + { "fli", ContentType.Fli }, + { "flo", ContentType.Flo }, + { "flv", ContentType.Flv }, + { "flw", ContentType.Flw }, + { "flx", ContentType.Flx }, + { "fly", ContentType.Fly }, + { "fm", ContentType.Fm }, + { "fnc", ContentType.Fnc }, + { "fo", ContentType.Fo }, + { "for", ContentType.For }, + { "fpx", ContentType.Fpx }, + { "frame", ContentType.Frame }, + { "fsc", ContentType.Fsc }, + { "fst", ContentType.Fst }, + { "ftc", ContentType.Ftc }, + { "fti", ContentType.Fti }, + { "fvt", ContentType.Fvt }, + { "fxp", ContentType.Fxp }, + { "fxpl", ContentType.Fxpl }, + { "fzs", ContentType.Fzs }, + { "g2w", ContentType.G2w }, + { "g3", ContentType.G3 }, + { "g3w", ContentType.G3w }, + { "gac", ContentType.Gac }, + { "gam", ContentType.Gam }, + { "gbr", ContentType.Gbr }, + { "gca", ContentType.Gca }, + { "gdl", ContentType.Gdl }, + { "gdoc", ContentType.Gdoc }, + { "geo", ContentType.Geo }, + { "geojson", ContentType.Geojson }, + { "gex", ContentType.Gex }, + { "ggb", ContentType.Ggb }, + { "ggt", ContentType.Ggt }, + { "ghf", ContentType.Ghf }, + { "gif", ContentType.Gif }, + { "gim", ContentType.Gim }, + { "glb", ContentType.Glb }, + { "gltf", ContentType.Gltf }, + { "gml", ContentType.Gml }, + { "gmx", ContentType.Gmx }, + { "gnumeric", ContentType.Gnumeric }, + { "gph", ContentType.Gph }, + { "gpx", ContentType.Gpx }, + { "gqf", ContentType.Gqf }, + { "gqs", ContentType.Gqs }, + { "gram", ContentType.Gram }, + { "gramps", ContentType.Gramps }, + { "gre", ContentType.Gre }, + { "grv", ContentType.Grv }, + { "grxml", ContentType.Grxml }, + { "gsf", ContentType.Gsf }, + { "gsheet", ContentType.Gsheet }, + { "gslides", ContentType.Gslides }, + { "gtar", ContentType.Gtar }, + { "gtm", ContentType.Gtm }, + { "gtw", ContentType.Gtw }, + { "gv", ContentType.Gv }, + { "gxf", ContentType.Gxf }, + { "gxt", ContentType.Gxt }, + { "gz", ContentType.Gz }, + { "h", ContentType.H }, + { "h261", ContentType.H261 }, + { "h263", ContentType.H263 }, + { "h264", ContentType.H264 }, + { "hal", ContentType.Hal }, + { "hbci", ContentType.Hbci }, + { "hbs", ContentType.Hbs }, + { "hdd", ContentType.Hdd }, + { "hdf", ContentType.Hdf }, + { "heic", ContentType.Heic }, + { "heics", ContentType.Heics }, + { "heif", ContentType.Heif }, + { "heifs", ContentType.Heifs }, + { "hej2", ContentType.Hej2 }, + { "held", ContentType.Held }, + { "hh", ContentType.Hh }, + { "hjson", ContentType.Hjson }, + { "hlp", ContentType.Hlp }, + { "hpgl", ContentType.Hpgl }, + { "hpid", ContentType.Hpid }, + { "hps", ContentType.Hps }, + { "hqx", ContentType.Hqx }, + { "hsj2", ContentType.Hsj2 }, + { "htc", ContentType.Htc }, + { "htke", ContentType.Htke }, + { "htm", ContentType.Htm }, + { "html", ContentType.Html }, + { "hvd", ContentType.Hvd }, + { "hvp", ContentType.Hvp }, + { "hex", ContentType.Binary }, + { "hvs", ContentType.Hvs }, + { "i2g", ContentType.I2g }, + { "icc", ContentType.Icc }, + { "ice", ContentType.Ice }, + { "icm", ContentType.Icm }, + { "ico", ContentType.Ico }, + { "ics", ContentType.Ics }, + { "ief", ContentType.Ief }, + { "ifb", ContentType.Ifb }, + { "ifm", ContentType.Ifm }, + { "iges", ContentType.Iges }, + { "igl", ContentType.Igl }, + { "igm", ContentType.Igm }, + { "igs", ContentType.Igs }, + { "igx", ContentType.Igx }, + { "iif", ContentType.Iif }, + { "img", ContentType.Img }, + { "imp", ContentType.Imp }, + { "ims", ContentType.Ims }, + { "ini", ContentType.Ini }, + { "ink", ContentType.Ink }, + { "inkml", ContentType.Inkml }, + { "install", ContentType.Install }, + { "iota", ContentType.Iota }, + { "ipfix", ContentType.Ipfix }, + { "ipk", ContentType.Ipk }, + { "irm", ContentType.Irm }, + { "irp", ContentType.Irp }, + { "iso", ContentType.Iso }, + { "itp", ContentType.Itp }, + { "its", ContentType.Its }, + { "ivp", ContentType.Ivp }, + { "ivu", ContentType.Ivu }, + { "jad", ContentType.Jad }, + { "jade", ContentType.Jade }, + { "jam", ContentType.Jam }, + { "jar", ContentType.Jar }, + { "jardiff", ContentType.Jardiff }, + { "java", ContentType.Java }, + { "jhc", ContentType.Jhc }, + { "jisp", ContentType.Jisp }, + { "jls", ContentType.Jls }, + { "jlt", ContentType.Jlt }, + { "jng", ContentType.Jng }, + { "jnlp", ContentType.Jnlp }, + { "joda", ContentType.Joda }, + { "jp2", ContentType.Jp2 }, + { "jpe", ContentType.Jpe }, + { "jpeg", ContentType.Jpeg }, + { "jpf", ContentType.Jpf }, + { "jpg", ContentType.Jpg }, + { "jpg2", ContentType.Jpg2 }, + { "jpgm", ContentType.Jpgm }, + { "jpgv", ContentType.Jpgv }, + { "jph", ContentType.Jph }, + { "jpm", ContentType.Jpm }, + { "jpx", ContentType.Jpx }, + { "js", ContentType.Javascript }, + { "json", ContentType.Json }, + { "json5", ContentType.Json5 }, + { "jsonld", ContentType.Jsonld }, + { "jsonml", ContentType.Jsonml }, + { "jsx", ContentType.Jsx }, + { "jxr", ContentType.Jxr }, + { "jxra", ContentType.Jxra }, + { "jxrs", ContentType.Jxrs }, + { "jxs", ContentType.Jxs }, + { "jxsc", ContentType.Jxsc }, + { "jxsi", ContentType.Jxsi }, + { "jxss", ContentType.Jxss }, + { "kar", ContentType.Kar }, + { "karbon", ContentType.Karbon }, + { "kdbx", ContentType.Kdbx }, + { "key", ContentType.Key }, + { "kfo", ContentType.Kfo }, + { "kia", ContentType.Kia }, + { "kml", ContentType.Kml }, + { "kmz", ContentType.Kmz }, + { "kne", ContentType.Kne }, + { "knp", ContentType.Knp }, + { "kon", ContentType.Kon }, + { "kpr", ContentType.Kpr }, + { "kpt", ContentType.Kpt }, + { "kpxx", ContentType.Kpxx }, + { "ksp", ContentType.Ksp }, + { "ktr", ContentType.Ktr }, + { "ktx", ContentType.Ktx }, + { "ktx2", ContentType.Ktx2 }, + { "ktz", ContentType.Ktz }, + { "kwd", ContentType.Kwd }, + { "kwt", ContentType.Kwt }, + { "lasxml", ContentType.Lasxml }, + { "latex", ContentType.Latex }, + { "lbd", ContentType.Lbd }, + { "lbe", ContentType.Lbe }, + { "les", ContentType.Les }, + { "less", ContentType.Less }, + { "lgr", ContentType.Lgr }, + { "lha", ContentType.Lha }, + { "link66", ContentType.Link66 }, + { "list", ContentType.List }, + { "list3820", ContentType.List3820 }, + { "listafp", ContentType.Listafp }, + { "lnk", ContentType.Lnk }, + { "log", ContentType.Log }, + { "lostxml", ContentType.Lostxml }, + { "lrf", ContentType.Lrf }, + { "lrm", ContentType.Lrm }, + { "ltf", ContentType.Ltf }, + { "lua", ContentType.Lua }, + { "luac", ContentType.Luac }, + { "lvp", ContentType.Lvp }, + { "lwp", ContentType.Lwp }, + { "lzh", ContentType.Lzh }, + { "m13", ContentType.M13 }, + { "m14", ContentType.M14 }, + { "m1v", ContentType.M1v }, + { "m21", ContentType.M21 }, + { "m2a", ContentType.M2a }, + { "m2v", ContentType.M2v }, + { "m3a", ContentType.M3a }, + { "m3u", ContentType.M3u }, + { "m3u8", ContentType.M3u8 }, + { "m4a", ContentType.M4a }, + { "m4p", ContentType.M4p }, + { "m4s", ContentType.M4s }, + { "m4u", ContentType.M4u }, + { "m4v", ContentType.M4v }, + { "ma", ContentType.Ma }, + { "mads", ContentType.Mads }, + { "maei", ContentType.Maei }, + { "mag", ContentType.Mag }, + { "maker", ContentType.Maker }, + { "man", ContentType.Man }, + { "manifest", ContentType.Manifest }, + { "map", ContentType.Map }, + { "mar", ContentType.Mar }, + { "markdown", ContentType.Markdown }, + { "mathml", ContentType.Mathml }, + { "mb", ContentType.Mb }, + { "mbk", ContentType.Mbk }, + { "mbox", ContentType.Mbox }, + { "mc1", ContentType.Mc1 }, + { "mcd", ContentType.Mcd }, + { "mcurl", ContentType.Mcurl }, + { "md", ContentType.Md }, + { "mdb", ContentType.Mdb }, + { "mdi", ContentType.Mdi }, + { "mdx", ContentType.Mdx }, + { "me", ContentType.Me }, + { "mesh", ContentType.Mesh }, + { "meta4", ContentType.Meta4 }, + { "metalink", ContentType.Metalink }, + { "mets", ContentType.Mets }, + { "mfm", ContentType.Mfm }, + { "mft", ContentType.Mft }, + { "mgp", ContentType.Mgp }, + { "mgz", ContentType.Mgz }, + { "mid", ContentType.Mid }, + { "midi", ContentType.Midi }, + { "mie", ContentType.Mie }, + { "mif", ContentType.Mif }, + { "mime", ContentType.Mime }, + { "mj2", ContentType.Mj2 }, + { "mjp2", ContentType.Mjp2 }, + { "mjs", ContentType.Mjs }, + { "mk3d", ContentType.Mk3d }, + { "mka", ContentType.Mka }, + { "mkd", ContentType.Mkd }, + { "mks", ContentType.Mks }, + { "mkv", ContentType.Mkv }, + { "mlp", ContentType.Mlp }, + { "mmd", ContentType.Mmd }, + { "mmf", ContentType.Mmf }, + { "mml", ContentType.Mml }, + { "mmr", ContentType.Mmr }, + { "mng", ContentType.Mng }, + { "mny", ContentType.Mny }, + { "mobi", ContentType.Mobi }, + { "mods", ContentType.Mods }, + { "mov", ContentType.Mov }, + { "movie", ContentType.Movie }, + { "mp2", ContentType.Mp2 }, + { "mp21", ContentType.Mp21 }, + { "mp2a", ContentType.Mp2a }, + { "mp3", ContentType.Mp3 }, + { "mp4", ContentType.Mp4 }, + { "mp4a", ContentType.Mp4a }, + { "mp4s", ContentType.Mp4s }, + { "mp4v", ContentType.Mp4v }, + { "mpc", ContentType.Mpc }, + { "mpd", ContentType.Mpd }, + { "mpe", ContentType.Mpe }, + { "mpeg", ContentType.Mpeg }, + { "mpg", ContentType.Mpg }, + { "mpg4", ContentType.Mpg4 }, + { "mpga", ContentType.Mpga }, + { "mpkg", ContentType.Mpkg }, + { "mpm", ContentType.Mpm }, + { "mpn", ContentType.Mpn }, + { "mpp", ContentType.Mpp }, + { "mpt", ContentType.Mpt }, + { "mpy", ContentType.Mpy }, + { "mqy", ContentType.Mqy }, + { "mrc", ContentType.Mrc }, + { "mrcx", ContentType.Mrcx }, + { "ms", ContentType.Ms }, + { "mscml", ContentType.Mscml }, + { "mseed", ContentType.Mseed }, + { "mseq", ContentType.Mseq }, + { "msf", ContentType.Msf }, + { "msg", ContentType.Msg }, + { "msh", ContentType.Msh }, + { "msi", ContentType.Msi }, + { "msl", ContentType.Msl }, + { "msm", ContentType.Msm }, + { "msp", ContentType.Msp }, + { "msty", ContentType.Msty }, + { "mtl", ContentType.Mtl }, + { "mts", ContentType.Mts }, + { "mus", ContentType.Mus }, + { "musd", ContentType.Musd }, + { "musicxml", ContentType.Musicxml }, + { "mvb", ContentType.Mvb }, + { "mvt", ContentType.Mvt }, + { "mwf", ContentType.Mwf }, + { "mxf", ContentType.Mxf }, + { "mxl", ContentType.Mxl }, + { "mxmf", ContentType.Mxmf }, + { "mxml", ContentType.Mxml }, + { "mxs", ContentType.Mxs }, + { "mxu", ContentType.Mxu }, + { "n3", ContentType.N3 }, + { "nb", ContentType.Nb }, + { "nbp", ContentType.Nbp }, + { "nc", ContentType.Nc }, + { "ncx", ContentType.Ncx }, + { "nfo", ContentType.Nfo }, + { "ngdat", ContentType.Ngdat }, + { "nitf", ContentType.Nitf }, + { "nlu", ContentType.Nlu }, + { "nml", ContentType.Nml }, + { "nnd", ContentType.Nnd }, + { "nns", ContentType.Nns }, + { "nnw", ContentType.Nnw }, + { "npx", ContentType.Npx }, + { "nq", ContentType.Nq }, + { "nsc", ContentType.Nsc }, + { "nsf", ContentType.Nsf }, + { "nt", ContentType.Nt }, + { "ntf", ContentType.Ntf }, + { "numbers", ContentType.Numbers }, + { "nzb", ContentType.Nzb }, + { "oa2", ContentType.Oa2 }, + { "oa3", ContentType.Oa3 }, + { "oas", ContentType.Oas }, + { "obd", ContentType.Obd }, + { "obgx", ContentType.Obgx }, + { "obj", ContentType.Obj }, + { "oda", ContentType.Oda }, + { "odb", ContentType.Odb }, + { "odc", ContentType.Odc }, + { "odf", ContentType.Odf }, + { "odft", ContentType.Odft }, + { "odg", ContentType.Odg }, + { "odi", ContentType.Odi }, + { "odm", ContentType.Odm }, + { "odp", ContentType.Odp }, + { "ods", ContentType.Ods }, + { "odt", ContentType.Odt }, + { "oga", ContentType.Oga }, + { "ogex", ContentType.Ogex }, + { "ogg", ContentType.Ogg }, + { "ogv", ContentType.Ogv }, + { "ogx", ContentType.Ogx }, + { "omdoc", ContentType.Omdoc }, + { "onepkg", ContentType.Onepkg }, + { "onetmp", ContentType.Onetmp }, + { "onetoc", ContentType.Onetoc }, + { "onetoc2", ContentType.Onetoc2 }, + { "opf", ContentType.Opf }, + { "opml", ContentType.Opml }, + { "oprc", ContentType.Oprc }, + { "opus", ContentType.Opus }, + { "org", ContentType.Org }, + { "osf", ContentType.Osf }, + { "osfpvg", ContentType.Osfpvg }, + { "osm", ContentType.Osm }, + { "otc", ContentType.Otc }, + { "otf", ContentType.Otf }, + { "otg", ContentType.Otg }, + { "oth", ContentType.Oth }, + { "oti", ContentType.Oti }, + { "otp", ContentType.Otp }, + { "ots", ContentType.Ots }, + { "ott", ContentType.Ott }, + { "ova", ContentType.Ova }, + { "ovf", ContentType.Ovf }, + { "owl", ContentType.Owl }, + { "oxps", ContentType.Oxps }, + { "oxt", ContentType.Oxt }, + { "p", ContentType.P }, + { "p10", ContentType.P10 }, + { "p12", ContentType.P12 }, + { "p7b", ContentType.P7b }, + { "p7c", ContentType.P7c }, + { "p7m", ContentType.P7m }, + { "p7r", ContentType.P7r }, + { "p7s", ContentType.P7s }, + { "p8", ContentType.P8 }, + { "pac", ContentType.Pac }, + { "pages", ContentType.Pages }, + { "pas", ContentType.Pas }, + { "paw", ContentType.Paw }, + { "pbd", ContentType.Pbd }, + { "pbm", ContentType.Pbm }, + { "pcap", ContentType.Pcap }, + { "pcf", ContentType.Pcf }, + { "pcl", ContentType.Pcl }, + { "pclxl", ContentType.Pclxl }, + { "pct", ContentType.Pct }, + { "pcurl", ContentType.Pcurl }, + { "pcx", ContentType.Pcx }, + { "pdb", ContentType.Pdb }, + { "pde", ContentType.Pde }, + { "pdf", ContentType.Pdf }, + { "pem", ContentType.Pem }, + { "pfa", ContentType.Pfa }, + { "pfb", ContentType.Pfb }, + { "pfm", ContentType.Pfm }, + { "pfr", ContentType.Pfr }, + { "pfx", ContentType.Pfx }, + { "pgm", ContentType.Pgm }, + { "pgn", ContentType.Pgn }, + { "pgp", ContentType.Pgp }, + { "php", ContentType.Php }, + { "pic", ContentType.Pic }, + { "pkg", ContentType.Pkg }, + { "pki", ContentType.Pki }, + { "pkipath", ContentType.Pkipath }, + { "pkpass", ContentType.Pkpass }, + { "pl", ContentType.Pl }, + { "plb", ContentType.Plb }, + { "plc", ContentType.Plc }, + { "plf", ContentType.Plf }, + { "pls", ContentType.Pls }, + { "pm", ContentType.Pm }, + { "pml", ContentType.Pml }, + { "png", ContentType.Png }, + { "pnm", ContentType.Pnm }, + { "portpkg", ContentType.Portpkg }, + { "pot", ContentType.Pot }, + { "potm", ContentType.Potm }, + { "potx", ContentType.Potx }, + { "ppam", ContentType.Ppam }, + { "ppd", ContentType.Ppd }, + { "ppm", ContentType.Ppm }, + { "pps", ContentType.Pps }, + { "ppsm", ContentType.Ppsm }, + { "ppsx", ContentType.Ppsx }, + { "ppt", ContentType.Ppt }, + { "pptm", ContentType.Pptm }, + { "pptx", ContentType.Pptx }, + { "pqa", ContentType.Pqa }, + { "prc", ContentType.Prc }, + { "pre", ContentType.Pre }, + { "prf", ContentType.Prf }, + { "provx", ContentType.Provx }, + { "ps", ContentType.Ps }, + { "psb", ContentType.Psb }, + { "psd", ContentType.Psd }, + { "psf", ContentType.Psf }, + { "pskcxml", ContentType.Pskcxml }, + { "pti", ContentType.Pti }, + { "ptid", ContentType.Ptid }, + { "pub", ContentType.Pub }, + { "pvb", ContentType.Pvb }, + { "pwn", ContentType.Pwn }, + { "pya", ContentType.Pya }, + { "pyv", ContentType.Pyv }, + { "qam", ContentType.Qam }, + { "qbo", ContentType.Qbo }, + { "qfx", ContentType.Qfx }, + { "qps", ContentType.Qps }, + { "qt", ContentType.Qt }, + { "qwd", ContentType.Qwd }, + { "qwt", ContentType.Qwt }, + { "qxb", ContentType.Qxb }, + { "qxd", ContentType.Qxd }, + { "qxl", ContentType.Qxl }, + { "qxt", ContentType.Qxt }, + { "ra", ContentType.Ra }, + { "ram", ContentType.Ram }, + { "raml", ContentType.Raml }, + { "rapd", ContentType.Rapd }, + { "rar", ContentType.Rar }, + { "ras", ContentType.Ras }, + { "rdf", ContentType.Rdf }, + { "rdz", ContentType.Rdz }, + { "relo", ContentType.Relo }, + { "rep", ContentType.Rep }, + { "res", ContentType.Res }, + { "rgb", ContentType.Rgb }, + { "rif", ContentType.Rif }, + { "rip", ContentType.Rip }, + { "ris", ContentType.Ris }, + { "rl", ContentType.Rl }, + { "rlc", ContentType.Rlc }, + { "rld", ContentType.Rld }, + { "rm", ContentType.Rm }, + { "rmi", ContentType.Rmi }, + { "rmp", ContentType.Rmp }, + { "rms", ContentType.Rms }, + { "rmvb", ContentType.Rmvb }, + { "rnc", ContentType.Rnc }, + { "rng", ContentType.Rng }, + { "roa", ContentType.Roa }, + { "roff", ContentType.Roff }, + { "rp9", ContentType.Rp9 }, + { "rpm", ContentType.Rpm }, + { "rpss", ContentType.Rpss }, + { "rpst", ContentType.Rpst }, + { "rq", ContentType.Rq }, + { "rs", ContentType.Rs }, + { "rsat", ContentType.Rsat }, + { "rsd", ContentType.Rsd }, + { "rsheet", ContentType.Rsheet }, + { "rss", ContentType.Rss }, + { "rtf", ContentType.Rtf }, + { "rtx", ContentType.Rtx }, + { "run", ContentType.Run }, + { "rusd", ContentType.Rusd }, + { "s", ContentType.S }, + { "s3m", ContentType.S3m }, + { "saf", ContentType.Saf }, + { "sass", ContentType.Sass }, + { "sbml", ContentType.Sbml }, + { "sc", ContentType.Sc }, + { "scd", ContentType.Scd }, + { "scm", ContentType.Scm }, + { "scq", ContentType.Scq }, + { "scs", ContentType.Scs }, + { "scss", ContentType.Scss }, + { "scurl", ContentType.Scurl }, + { "sda", ContentType.Sda }, + { "sdc", ContentType.Sdc }, + { "sdd", ContentType.Sdd }, + { "sdkd", ContentType.Sdkd }, + { "sdkm", ContentType.Sdkm }, + { "sdp", ContentType.Sdp }, + { "sdw", ContentType.Sdw }, + { "sea", ContentType.Sea }, + { "see", ContentType.See }, + { "seed", ContentType.Seed }, + { "sema", ContentType.Sema }, + { "semd", ContentType.Semd }, + { "semf", ContentType.Semf }, + { "senmlx", ContentType.Senmlx }, + { "sensmlx", ContentType.Sensmlx }, + { "ser", ContentType.Ser }, + { "setpay", ContentType.Setpay }, + { "setreg", ContentType.Setreg }, + { "sfs", ContentType.Sfs }, + { "sfv", ContentType.Sfv }, + { "sgi", ContentType.Sgi }, + { "sgl", ContentType.Sgl }, + { "sgm", ContentType.Sgm }, + { "sgml", ContentType.Sgml }, + { "sh", ContentType.Sh }, + { "shar", ContentType.Shar }, + { "shex", ContentType.Shex }, + { "shf", ContentType.Shf }, + { "shtml", ContentType.Shtml }, + { "sid", ContentType.Sid }, + { "sieve", ContentType.Sieve }, + { "sig", ContentType.Sig }, + { "sil", ContentType.Sil }, + { "silo", ContentType.Silo }, + { "sis", ContentType.Sis }, + { "sisx", ContentType.Sisx }, + { "sit", ContentType.Sit }, + { "sitx", ContentType.Sitx }, + { "siv", ContentType.Siv }, + { "skd", ContentType.Skd }, + { "skm", ContentType.Skm }, + { "skp", ContentType.Skp }, + { "skt", ContentType.Skt }, + { "sldm", ContentType.Sldm }, + { "sldx", ContentType.Sldx }, + { "slim", ContentType.Slim }, + { "slm", ContentType.Slm }, + { "sls", ContentType.Sls }, + { "slt", ContentType.Slt }, + { "sm", ContentType.Sm }, + { "smf", ContentType.Smf }, + { "smi", ContentType.Smi }, + { "smil", ContentType.Smil }, + { "smv", ContentType.Smv }, + { "smzip", ContentType.Smzip }, + { "snd", ContentType.Snd }, + { "snf", ContentType.Snf }, + { "so", ContentType.So }, + { "spc", ContentType.Spc }, + { "spdx", ContentType.Spdx }, + { "spf", ContentType.Spf }, + { "spl", ContentType.Spl }, + { "spot", ContentType.Spot }, + { "spp", ContentType.Spp }, + { "spq", ContentType.Spq }, + { "spx", ContentType.Spx }, + { "sql", ContentType.Sql }, + { "src", ContentType.Src }, + { "srt", ContentType.Srt }, + { "sru", ContentType.Sru }, + { "srx", ContentType.Srx }, + { "ssdl", ContentType.Ssdl }, + { "sse", ContentType.Sse }, + { "ssf", ContentType.Ssf }, + { "ssml", ContentType.Ssml }, + { "st", ContentType.St }, + { "stc", ContentType.Stc }, + { "std", ContentType.Std }, + { "stf", ContentType.Stf }, + { "sti", ContentType.Sti }, + { "stk", ContentType.Stk }, + { "stl", ContentType.Stl }, + { "stpx", ContentType.Stpx }, + { "stpxz", ContentType.Stpxz }, + { "stpz", ContentType.Stpz }, + { "str", ContentType.Str }, + { "stw", ContentType.Stw }, + { "styl", ContentType.Styl }, + { "stylus", ContentType.Stylus }, + { "sub", ContentType.Sub }, + { "sus", ContentType.Sus }, + { "susp", ContentType.Susp }, + { "sv4cpio", ContentType.Sv4cpio }, + { "sv4crc", ContentType.Sv4crc }, + { "svc", ContentType.Svc }, + { "svd", ContentType.Svd }, + { "svg", ContentType.Svg }, + { "svgz", ContentType.Svgz }, + { "swa", ContentType.Swa }, + { "swf", ContentType.Swf }, + { "swi", ContentType.Swi }, + { "swidtag", ContentType.Swidtag }, + { "sxc", ContentType.Sxc }, + { "sxd", ContentType.Sxd }, + { "sxg", ContentType.Sxg }, + { "sxi", ContentType.Sxi }, + { "sxm", ContentType.Sxm }, + { "sxw", ContentType.Sxw }, + { "t", ContentType.T }, + { "t3", ContentType.T3 }, + { "t38", ContentType.T38 }, + { "taglet", ContentType.Taglet }, + { "tao", ContentType.Tao }, + { "tap", ContentType.Tap }, + { "tar", ContentType.Tar }, + { "tcap", ContentType.Tcap }, + { "tcl", ContentType.Tcl }, + { "td", ContentType.Td }, + { "teacher", ContentType.Teacher }, + { "tei", ContentType.Tei }, + { "tex", ContentType.Tex }, + { "texi", ContentType.Texi }, + { "texinfo", ContentType.Texinfo }, + { "text", ContentType.Text }, + { "tfi", ContentType.Tfi }, + { "tfm", ContentType.Tfm }, + { "tfx", ContentType.Tfx }, + { "tga", ContentType.Tga }, + { "thmx", ContentType.Thmx }, + { "tif", ContentType.Tif }, + { "tiff", ContentType.Tiff }, + { "tk", ContentType.Tk }, + { "tmo", ContentType.Tmo }, + { "toml", ContentType.Toml }, + { "torrent", ContentType.Torrent }, + { "tpl", ContentType.Tpl }, + { "tpt", ContentType.Tpt }, + { "tr", ContentType.Tr }, + { "tra", ContentType.Tra }, + { "trig", ContentType.Trig }, + { "trm", ContentType.Trm }, + { "ts", ContentType.Ts }, + { "tsd", ContentType.Tsd }, + { "tsv", ContentType.Tsv }, + { "ttc", ContentType.Ttc }, + { "ttf", ContentType.Ttf }, + { "ttl", ContentType.Ttl }, + { "ttml", ContentType.Ttml }, + { "twd", ContentType.Twd }, + { "twds", ContentType.Twds }, + { "txd", ContentType.Txd }, + { "txf", ContentType.Txf }, + { "txt", ContentType.Txt }, + { "u32", ContentType.U32 }, + { "u8dsn", ContentType.U8dsn }, + { "u8hdr", ContentType.U8hdr }, + { "u8mdn", ContentType.U8mdn }, + { "u8msg", ContentType.U8msg }, + { "ubj", ContentType.Ubj }, + { "udeb", ContentType.Udeb }, + { "ufd", ContentType.Ufd }, + { "ufdl", ContentType.Ufdl }, + { "ulx", ContentType.Ulx }, + { "umj", ContentType.Umj }, + { "unityweb", ContentType.Unityweb }, + { "uoml", ContentType.Uoml }, + { "uri", ContentType.Uri }, + { "uris", ContentType.Uris }, + { "urls", ContentType.Urls }, + { "usdz", ContentType.Usdz }, + { "ustar", ContentType.Ustar }, + { "utz", ContentType.Utz }, + { "uu", ContentType.Uu }, + { "uva", ContentType.Uva }, + { "uvd", ContentType.Uvd }, + { "uvf", ContentType.Uvf }, + { "uvg", ContentType.Uvg }, + { "uvh", ContentType.Uvh }, + { "uvi", ContentType.Uvi }, + { "uvm", ContentType.Uvm }, + { "uvp", ContentType.Uvp }, + { "uvs", ContentType.Uvs }, + { "uvt", ContentType.Uvt }, + { "uvu", ContentType.Uvu }, + { "uvv", ContentType.Uvv }, + { "uvva", ContentType.Uvva }, + { "uvvd", ContentType.Uvvd }, + { "uvvf", ContentType.Uvvf }, + { "uvvg", ContentType.Uvvg }, + { "uvvh", ContentType.Uvvh }, + { "uvvi", ContentType.Uvvi }, + { "uvvm", ContentType.Uvvm }, + { "uvvp", ContentType.Uvvp }, + { "uvvs", ContentType.Uvvs }, + { "uvvt", ContentType.Uvvt }, + { "uvvu", ContentType.Uvvu }, + { "uvvv", ContentType.Uvvv }, + { "uvvx", ContentType.Uvvx }, + { "uvvz", ContentType.Uvvz }, + { "uvx", ContentType.Uvx }, + { "uvz", ContentType.Uvz }, + { "vbox", ContentType.Vbox }, + { "vcard", ContentType.Vcard }, + { "vcd", ContentType.Vcd }, + { "vcf", ContentType.Vcf }, + { "vcg", ContentType.Vcg }, + { "vcs", ContentType.Vcs }, + { "vcx", ContentType.Vcx }, + { "vdi", ContentType.Vdi }, + { "vds", ContentType.Vds }, + { "vhd", ContentType.Vhd }, + { "vis", ContentType.Vis }, + { "viv", ContentType.Viv }, + { "vmdk", ContentType.Vmdk }, + { "vob", ContentType.Vob }, + { "vor", ContentType.Vor }, + { "vox", ContentType.Vox }, + { "vrml", ContentType.Vrml }, + { "vsd", ContentType.Vsd }, + { "vsf", ContentType.Vsf }, + { "vss", ContentType.Vss }, + { "vst", ContentType.Vst }, + { "vsw", ContentType.Vsw }, + { "vtf", ContentType.Vtf }, + { "vtt", ContentType.Vtt }, + { "vtu", ContentType.Vtu }, + { "vxml", ContentType.Vxml }, + { "w3d", ContentType.W3d }, + { "wad", ContentType.Wad }, + { "wadl", ContentType.Wadl }, + { "war", ContentType.War }, + { "wasm", ContentType.Wasm }, + { "wav", ContentType.Wav }, + { "wax", ContentType.Wax }, + { "wbmp", ContentType.Wbmp }, + { "wbs", ContentType.Wbs }, + { "wbxml", ContentType.Wbxml }, + { "wcm", ContentType.Wcm }, + { "wdb", ContentType.Wdb }, + { "wdp", ContentType.Wdp }, + { "weba", ContentType.Weba }, + { "webapp", ContentType.Webapp }, + { "webm", ContentType.Webm }, + { "webp", ContentType.Webp }, + { "wg", ContentType.Wg }, + { "wgt", ContentType.Wgt }, + { "wks", ContentType.Wks }, + { "wm", ContentType.Wm }, + { "wma", ContentType.Wma }, + { "wmd", ContentType.Wmd }, + { "wmf", ContentType.Wmf }, + { "wml", ContentType.Wml }, + { "wmlc", ContentType.Wmlc }, + { "wmls", ContentType.Wmls }, + { "wmlsc", ContentType.Wmlsc }, + { "wmv", ContentType.Wmv }, + { "wmx", ContentType.Wmx }, + { "wmz", ContentType.Wmz }, + { "woff", ContentType.Woff }, + { "woff2", ContentType.Woff2 }, + { "wpd", ContentType.Wpd }, + { "wpl", ContentType.Wpl }, + { "wps", ContentType.Wps }, + { "wqd", ContentType.Wqd }, + { "wri", ContentType.Wri }, + { "wrl", ContentType.Wrl }, + { "wsc", ContentType.Wsc }, + { "wsdl", ContentType.Wsdl }, + { "wspolicy", ContentType.Wspolicy }, + { "wtb", ContentType.Wtb }, + { "wvx", ContentType.Wvx }, + { "x32", ContentType.X32 }, + { "x3d", ContentType.X3d }, + { "x3db", ContentType.X3db }, + { "x3dbz", ContentType.X3dbz }, + { "x3dv", ContentType.X3dv }, + { "x3dvz", ContentType.X3dvz }, + { "x3dz", ContentType.X3dz }, + { "xaml", ContentType.Xaml }, + { "xap", ContentType.Xap }, + { "xar", ContentType.Xar }, + { "xav", ContentType.Xav }, + { "xbap", ContentType.Xbap }, + { "xbd", ContentType.Xbd }, + { "xbm", ContentType.Xbm }, + { "xca", ContentType.Xca }, + { "xcs", ContentType.Xcs }, + { "xdf", ContentType.Xdf }, + { "xdm", ContentType.Xdm }, + { "xdp", ContentType.Xdp }, + { "xdssc", ContentType.Xdssc }, + { "xdw", ContentType.Xdw }, + { "xel", ContentType.Xel }, + { "xenc", ContentType.Xenc }, + { "xer", ContentType.Xer }, + { "xfdf", ContentType.Xfdf }, + { "xfdl", ContentType.Xfdl }, + { "xht", ContentType.Xht }, + { "xhtml", ContentType.Xhtml }, + { "xhvml", ContentType.Xhvml }, + { "xif", ContentType.Xif }, + { "xla", ContentType.Xla }, + { "xlam", ContentType.Xlam }, + { "xlc", ContentType.Xlc }, + { "xlf", ContentType.Xlf }, + { "xlm", ContentType.Xlm }, + { "xls", ContentType.Xls }, + { "xlsb", ContentType.Xlsb }, + { "xlsm", ContentType.Xlsm }, + { "xlsx", ContentType.Xlsx }, + { "xlt", ContentType.Xlt }, + { "xltm", ContentType.Xltm }, + { "xltx", ContentType.Xltx }, + { "xlw", ContentType.Xlw }, + { "xm", ContentType.Xm }, + { "xml", ContentType.Xml }, + { "xns", ContentType.Xns }, + { "xo", ContentType.Xo }, + { "xop", ContentType.Xop }, + { "xpi", ContentType.Xpi }, + { "xpl", ContentType.Xpl }, + { "xpm", ContentType.Xpm }, + { "xpr", ContentType.Xpr }, + { "xps", ContentType.Xps }, + { "xpw", ContentType.Xpw }, + { "xpx", ContentType.Xpx }, + { "xsd", ContentType.Xsd }, + { "xsl", ContentType.Xsl }, + { "xslt", ContentType.Xslt }, + { "xsm", ContentType.Xsm }, + { "xspf", ContentType.Xspf }, + { "xul", ContentType.Xul }, + { "xvm", ContentType.Xvm }, + { "xvml", ContentType.Xvml }, + { "xwd", ContentType.Xwd }, + { "xyz", ContentType.Xyz }, + { "xz", ContentType.Xz }, + { "yaml", ContentType.Yaml }, + { "yang", ContentType.Yang }, + { "yin", ContentType.Yin }, + { "yml", ContentType.Yml }, + { "ymp", ContentType.Ymp }, + { "z1", ContentType.Z1 }, + { "z2", ContentType.Z2 }, + { "z3", ContentType.Z3 }, + { "z4", ContentType.Z4 }, + { "z5", ContentType.Z5 }, + { "z6", ContentType.Z6 }, + { "z7", ContentType.Z7 }, + { "z8", ContentType.Z8 }, + { "zaz", ContentType.Zaz }, + { "zip", ContentType.Zip }, + { "zir", ContentType.Zir }, + { "zirz", ContentType.Zirz }, + { "zmm", ContentType.Zmm }, + }; + private static readonly IReadOnlyDictionary<string, ContentType> MimeToCt = new Dictionary<string, ContentType>() + { + { "application/x-www-form-urlencoded", ContentType.UrlEncoded }, + { "multipart/form-data", ContentType.MultiPart }, + { "audio/x-aac", ContentType.Aac }, + { "application/x-authorware-map", ContentType.Aam }, + { "application/x-authorware-seg", ContentType.Aas }, + { "application/x-abiword", ContentType.Abw }, + { "application/pkix-attr-cert", ContentType.Ac }, + { "application/vnd.americandynamics.acc", ContentType.Acc }, + { "application/x-ace-compressed", ContentType.Ace }, + { "application/vnd.acucobol", ContentType.Acu }, + { "application/vnd.acucorp", ContentType.Acutc }, + { "audio/adpcm", ContentType.Adp }, + { "application/vnd.audiograph", ContentType.Aep }, + { "application/vnd.ibm.modcap", ContentType.Afp }, + { "application/vnd.ahead.space", ContentType.Ahead }, + { "audio/x-aiff", ContentType.Aiff }, + { "application/vnd.adobe.air-application-installer-package+zip", ContentType.Air }, + { "application/vnd.dvb.ait", ContentType.Ait }, + { "application/vnd.amiga.ami", ContentType.Ami }, + { "audio/amr", ContentType.Amr }, + { "application/vnd.android.package-archive", ContentType.Apk }, + { "image/apng", ContentType.Apng }, + { "application/vnd.lotus-approach", ContentType.Apr }, + { "application/x-freearc", ContentType.Arc }, + { "application/x-arj", ContentType.Arj }, + { "video/x-ms-asf", ContentType.Asf }, + { "text/x-asm", ContentType.Asm }, + { "application/vnd.accpac.simply.aso", ContentType.Aso }, + { "application/atom+xml", ContentType.Atom }, + { "application/atomcat+xml", ContentType.Atomcat }, + { "application/atomsvc+xml", ContentType.Atomsvc }, + { "application/vnd.antix.game-component", ContentType.Atx }, + { "audio/basic", ContentType.Au }, + { "video/x-msvideo", ContentType.Avi }, + { "image/avif", ContentType.Avif }, + { "application/applixware", ContentType.Aw }, + { "application/vnd.airzip.filesecure.azf", ContentType.Azf }, + { "application/vnd.airzip.filesecure.azs", ContentType.Azs }, + { "image/vnd.airzip.accelerator.azv", ContentType.Azv }, + { "application/vnd.amazon.ebook", ContentType.Azw }, + { "image/vnd.pco.b16", ContentType.B16 }, + { "application/x-msdownload", ContentType.Bat }, + { "application/x-bcpio", ContentType.Bcpio }, + { "application/x-font-bdf", ContentType.Bdf }, + { "application/vnd.syncml.dm+wbxml", ContentType.Bdm }, + { "application/bdoc", ContentType.Bdoc }, + { "application/vnd.realvnc.bed", ContentType.Bed }, + { "application/vnd.fujitsu.oasysprs", ContentType.Bh2 }, + { "application/octet-stream", ContentType.Binary }, + { "application/x-blorb", ContentType.Blorb }, + { "application/vnd.bmi", ContentType.Bmi }, + { "application/vnd.balsamiq.bmml+xml", ContentType.Bmml }, + { "image/bmp", ContentType.Bmp }, + { "application/vnd.previewsystems.box", ContentType.Box }, + { "application/x-bzip2", ContentType.Boz }, + { "model/vnd.valve.source.compiled-map", ContentType.Bsp }, + { "image/prs.btif", ContentType.Btif }, + { "application/x-bzip", ContentType.Bz }, + { "text/x-c", ContentType.C }, + { "application/vnd.cluetrust.cartomobile-config", ContentType.C11amc }, + { "application/vnd.cluetrust.cartomobile-config-pkg", ContentType.C11amz }, + { "application/vnd.clonk.c4group", ContentType.C4d }, + { "application/vnd.ms-cab-compressed", ContentType.Cab }, + { "audio/x-caf", ContentType.Caf }, + { "application/vnd.curl.car", ContentType.Car }, + { "application/vnd.ms-pki.seccat", ContentType.Cat }, + { "application/x-cbr", ContentType.Cb7 }, + { "application/x-cocoa", ContentType.Cco }, + { "application/ccxml+xml", ContentType.Ccxml }, + { "application/vnd.contact.cmsg", ContentType.Cdbcmsg }, + { "application/x-netcdf", ContentType.Cdf }, + { "application/cdfx+xml", ContentType.Cdfx }, + { "application/vnd.mediastation.cdkey", ContentType.Cdkey }, + { "application/cdmi-capability", ContentType.Cdmia }, + { "application/cdmi-container", ContentType.Cdmic }, + { "application/cdmi-domain", ContentType.Cdmid }, + { "application/cdmi-object", ContentType.Cdmio }, + { "application/cdmi-queue", ContentType.Cdmiq }, + { "chemical/x-cdx", ContentType.Cdx }, + { "application/vnd.chemdraw+xml", ContentType.Cdxml }, + { "application/vnd.cinderella", ContentType.Cdy }, + { "application/pkix-cert", ContentType.Cer }, + { "application/x-cfs-compressed", ContentType.Cfs }, + { "image/cgm", ContentType.Cgm }, + { "application/x-chat", ContentType.Chat }, + { "application/vnd.ms-htmlhelp", ContentType.Chm }, + { "application/vnd.kde.kchart", ContentType.Chrt }, + { "chemical/x-cif", ContentType.Cif }, + { "application/vnd.anser-web-certificate-issue-initiation", ContentType.Cii }, + { "application/vnd.ms-artgalry", ContentType.Cil }, + { "application/node", ContentType.Cjs }, + { "application/vnd.claymore", ContentType.Cla }, + { "application/vnd.crick.clicker.keyboard", ContentType.Clkk }, + { "application/vnd.crick.clicker.palette", ContentType.Clkp }, + { "application/vnd.crick.clicker.template", ContentType.Clkt }, + { "application/vnd.crick.clicker.wordbank", ContentType.Clkw }, + { "application/vnd.crick.clicker", ContentType.Clkx }, + { "application/x-msclip", ContentType.Clp }, + { "application/vnd.cosmocaller", ContentType.Cmc }, + { "chemical/x-cmdf", ContentType.Cmdf }, + { "chemical/x-cml", ContentType.Cml }, + { "application/vnd.yellowriver-custom-menu", ContentType.Cmp }, + { "image/x-cmx", ContentType.Cmx }, + { "application/vnd.rim.cod", ContentType.Cod }, + { "text/coffeescript", ContentType.Coffee }, + { "application/x-cpio", ContentType.Cpio }, + { "application/mac-compactpro", ContentType.Cpt }, + { "application/x-mscardfile", ContentType.Crd }, + { "application/pkix-crl", ContentType.Crl }, + { "application/x-x509-ca-cert", ContentType.Crt }, + { "application/x-chrome-extension", ContentType.Crx }, + { "application/x-csh", ContentType.Csh }, + { "application/vnd.citationstyles.style+xml", ContentType.Csl }, + { "chemical/x-csml", ContentType.Csml }, + { "application/vnd.commonspace", ContentType.Csp }, + { "text/css", ContentType.Css }, + { "text/csv", ContentType.Csv }, + { "application/cu-seeme", ContentType.Cu }, + { "text/vnd.curl", ContentType.Curl }, + { "application/prs.cww", ContentType.Cww }, + { "model/vnd.collada+xml", ContentType.Dae }, + { "application/vnd.mobius.daf", ContentType.Daf }, + { "application/vnd.dart", ContentType.Dart }, + { "application/davmount+xml", ContentType.Davmount }, + { "application/vnd.dbf", ContentType.Dbf }, + { "application/docbook+xml", ContentType.Dbk }, + { "application/x-director", ContentType.Dcr }, + { "text/vnd.curl.dcurl", ContentType.Dcurl }, + { "application/vnd.oma.dd2+xml", ContentType.Dd2 }, + { "application/vnd.fujixerox.ddd", ContentType.Ddd }, + { "application/vnd.syncml.dmddf+xml", ContentType.Ddf }, + { "image/vnd.ms-dds", ContentType.Dds }, + { "application/vnd.dreamfactory", ContentType.Dfac }, + { "application/x-dgc-compressed", ContentType.Dgc }, + { "application/vnd.mobius.dis", ContentType.Dis }, + { "image/vnd.djvu", ContentType.Djvu }, + { "application/vnd.dna", ContentType.Dna }, + { "application/msword", ContentType.Doc }, + { "application/vnd.ms-word.document.macroenabled.12", ContentType.Docm }, + { "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ContentType.Docx }, + { "application/vnd.openxmlformats-officedocument.wordprocessingml.template", ContentType.Dotx }, + { "application/vnd.osgi.dp", ContentType.Dp }, + { "application/vnd.dpgraph", ContentType.Dpg }, + { "audio/vnd.dra", ContentType.Dra }, + { "image/dicom-rle", ContentType.Drle }, + { "text/prs.lines.tag", ContentType.Dsc }, + { "application/dssc+der", ContentType.Dssc }, + { "application/x-dtbook+xml", ContentType.Dtb }, + { "application/xml-dtd", ContentType.Dtd }, + { "audio/vnd.dts", ContentType.Dts }, + { "audio/vnd.dts.hd", ContentType.Dtshd }, + { "video/vnd.dvb.file", ContentType.Dvb }, + { "application/x-dvi", ContentType.Dvi }, + { "application/atsc-dwd+xml", ContentType.Dwd }, + { "model/vnd.dwf", ContentType.Dwf }, + { "image/vnd.dwg", ContentType.Dwg }, + { "image/vnd.dxf", ContentType.Dxf }, + { "application/vnd.spotfire.dxp", ContentType.Dxp }, + { "application/ecmascript", ContentType.Ecma }, + { "application/vnd.novadigm.edm", ContentType.Edm }, + { "application/vnd.novadigm.edx", ContentType.Edx }, + { "application/vnd.picsel", ContentType.Efif }, + { "application/vnd.pg.osasli", ContentType.Ei6 }, + { "application/emma+xml", ContentType.Emma }, + { "audio/vnd.digital-winds", ContentType.Eol }, + { "application/vnd.ms-fontobject", ContentType.Eot }, + { "application/epub+zip", ContentType.Epub }, + { "application/vnd.osgi.subsystem", ContentType.Esa }, + { "application/vnd.epson.esf", ContentType.Esf }, + { "application/vnd.eszigno3+xml", ContentType.Et3 }, + { "text/x-setext", ContentType.Etx }, + { "application/x-eva", ContentType.Eva }, + { "application/x-envoy", ContentType.Evy }, + { "application/exi", ContentType.Exi }, + { "application/express", ContentType.Exp }, + { "image/aces", ContentType.Exr }, + { "application/vnd.novadigm.ext", ContentType.Ext }, + { "application/andrew-inset", ContentType.Ez }, + { "application/vnd.ezpix-album", ContentType.Ez2 }, + { "application/vnd.ezpix-package", ContentType.Ez3 }, + { "video/x-f4v", ContentType.F4v }, + { "text/x-fortran", ContentType.Fortran }, + { "image/vnd.fastbidsheet", ContentType.Fbs }, + { "application/vnd.adobe.formscentral.fcdt", ContentType.Fcdt }, + { "application/vnd.isac.fcs", ContentType.Fcs }, + { "application/vnd.fdf", ContentType.Fdf }, + { "application/fdt+xml", ContentType.Fdt }, + { "application/vnd.fujitsu.oasysgp", ContentType.Fg5 }, + { "image/x-freehand", ContentType.Fh }, + { "application/x-xfig", ContentType.Fig }, + { "image/fits", ContentType.Fits }, + { "audio/x-flac", ContentType.Flac }, + { "video/x-fli", ContentType.Fli }, + { "application/vnd.micrografx.flo", ContentType.Flo }, + { "video/x-flv", ContentType.Flv }, + { "application/vnd.kde.kivio", ContentType.Flw }, + { "text/vnd.fmi.flexstor", ContentType.Flx }, + { "text/vnd.fly", ContentType.Fly }, + { "application/vnd.frogans.fnc", ContentType.Fnc }, + { "application/vnd.software602.filler.form+xml", ContentType.Fo }, + { "image/vnd.fpx", ContentType.Fpx }, + { "application/vnd.framemaker", ContentType.Frame }, + { "application/vnd.fsc.weblaunch", ContentType.Fsc }, + { "image/vnd.fst", ContentType.Fst }, + { "application/vnd.fluxtime.clip", ContentType.Ftc }, + { "application/vnd.anser-web-funds-transfer-initiation", ContentType.Fti }, + { "video/vnd.fvt", ContentType.Fvt }, + { "application/vnd.adobe.fxp", ContentType.Fxp }, + { "application/vnd.fuzzysheet", ContentType.Fzs }, + { "application/vnd.geoplan", ContentType.G2w }, + { "image/g3fax", ContentType.G3 }, + { "application/vnd.geospace", ContentType.G3w }, + { "application/vnd.groove-account", ContentType.Gac }, + { "application/x-tads", ContentType.Gam }, + { "application/rpki-ghostbusters", ContentType.Gbr }, + { "application/x-gca-compressed", ContentType.Gca }, + { "model/vnd.gdl", ContentType.Gdl }, + { "application/vnd.google-apps.document", ContentType.Gdoc }, + { "application/vnd.dynageo", ContentType.Geo }, + { "application/geo+json", ContentType.Geojson }, + { "application/vnd.geometry-explorer", ContentType.Gex }, + { "application/vnd.geogebra.file", ContentType.Ggb }, + { "application/vnd.geogebra.tool", ContentType.Ggt }, + { "application/vnd.groove-help", ContentType.Ghf }, + { "image/gif", ContentType.Gif }, + { "application/vnd.groove-identity-message", ContentType.Gim }, + { "model/gltf-binary", ContentType.Glb }, + { "model/gltf+json", ContentType.Gltf }, + { "application/gml+xml", ContentType.Gml }, + { "application/vnd.gmx", ContentType.Gmx }, + { "application/x-gnumeric", ContentType.Gnumeric }, + { "application/vnd.flographit", ContentType.Gph }, + { "application/gpx+xml", ContentType.Gpx }, + { "application/vnd.grafeq", ContentType.Gqf }, + { "application/srgs", ContentType.Gram }, + { "application/x-gramps-xml", ContentType.Gramps }, + { "application/vnd.groove-injector", ContentType.Grv }, + { "application/srgs+xml", ContentType.Grxml }, + { "application/x-font-ghostscript", ContentType.Gsf }, + { "application/vnd.google-apps.spreadsheet", ContentType.Gsheet }, + { "application/vnd.google-apps.presentation", ContentType.Gslides }, + { "application/x-gtar", ContentType.Gtar }, + { "application/vnd.groove-tool-message", ContentType.Gtm }, + { "model/vnd.gtw", ContentType.Gtw }, + { "text/vnd.graphviz", ContentType.Gv }, + { "application/gxf", ContentType.Gxf }, + { "application/vnd.geonext", ContentType.Gxt }, + { "application/gzip", ContentType.Gz }, + { "video/h261", ContentType.H261 }, + { "video/h263", ContentType.H263 }, + { "video/h264", ContentType.H264 }, + { "application/vnd.hal+xml", ContentType.Hal }, + { "application/vnd.hbci", ContentType.Hbci }, + { "text/x-handlebars-template", ContentType.Hbs }, + { "application/x-virtualbox-hdd", ContentType.Hdd }, + { "application/x-hdf", ContentType.Hdf }, + { "image/heic", ContentType.Heic }, + { "image/heic-sequence", ContentType.Heics }, + { "image/heif", ContentType.Heif }, + { "image/heif-sequence", ContentType.Heifs }, + { "image/hej2k", ContentType.Hej2 }, + { "application/atsc-held+xml", ContentType.Held }, + { "application/hjson", ContentType.Hjson }, + { "application/winhlp", ContentType.Hlp }, + { "application/vnd.hp-hpgl", ContentType.Hpgl }, + { "application/vnd.hp-hpid", ContentType.Hpid }, + { "application/vnd.hp-hps", ContentType.Hps }, + { "application/mac-binhex40", ContentType.Hqx }, + { "image/hsj2", ContentType.Hsj2 }, + { "application/vnd.kenameaapp", ContentType.Htke }, + { "text/html", ContentType.Html }, + { "application/vnd.yamaha.hv-dic", ContentType.Hvd }, + { "application/vnd.yamaha.hv-voice", ContentType.Hvp }, + { "application/vnd.yamaha.hv-script", ContentType.Hvs }, + { "application/vnd.intergeo", ContentType.I2g }, + { "application/vnd.iccprofile", ContentType.Icc }, + { "x-conference/x-cooltalk", ContentType.Ice }, + { "image/vnd.microsoft.icon", ContentType.Ico }, + { "image/ief", ContentType.Ief }, + { "application/vnd.shana.informed.formdata", ContentType.Ifm }, + { "model/iges", ContentType.Iges }, + { "application/vnd.igloader", ContentType.Igl }, + { "application/vnd.insors.igm", ContentType.Igm }, + { "application/vnd.micrografx.igx", ContentType.Igx }, + { "application/vnd.shana.informed.interchange", ContentType.Iif }, + { "application/vnd.accpac.simply.imp", ContentType.Imp }, + { "application/vnd.ms-ims", ContentType.Ims }, + { "application/inkml+xml", ContentType.Inkml }, + { "application/x-install-instructions", ContentType.Install }, + { "application/vnd.astraea-software.iota", ContentType.Iota }, + { "application/ipfix", ContentType.Ipfix }, + { "application/vnd.shana.informed.package", ContentType.Ipk }, + { "application/vnd.ibm.rights-management", ContentType.Irm }, + { "application/vnd.irepository.package+xml", ContentType.Irp }, + { "application/vnd.shana.informed.formtemplate", ContentType.Itp }, + { "application/its+xml", ContentType.Its }, + { "application/vnd.immervision-ivp", ContentType.Ivp }, + { "application/vnd.immervision-ivu", ContentType.Ivu }, + { "text/vnd.sun.j2me.app-descriptor", ContentType.Jad }, + { "text/jade", ContentType.Jade }, + { "application/vnd.jam", ContentType.Jam }, + { "application/java-archive", ContentType.Jar }, + { "application/x-java-archive-diff", ContentType.Jardiff }, + { "text/x-java-source", ContentType.Java }, + { "image/jphc", ContentType.Jhc }, + { "application/vnd.jisp", ContentType.Jisp }, + { "image/jls", ContentType.Jls }, + { "application/vnd.hp-jlyt", ContentType.Jlt }, + { "image/x-jng", ContentType.Jng }, + { "application/x-java-jnlp-file", ContentType.Jnlp }, + { "application/vnd.joost.joda-archive", ContentType.Joda }, + { "image/jp2", ContentType.Jp2 }, + { "image/jpeg", ContentType.Jpeg }, + { "video/jpm", ContentType.Jpgm }, + { "video/jpeg", ContentType.Jpgv }, + { "image/jph", ContentType.Jph }, + { "image/jpm", ContentType.Jpm }, + { "image/jpx", ContentType.Jpx }, + { "application/javascript", ContentType.Javascript }, + { "application/json", ContentType.Json }, + { "application/json5", ContentType.Json5 }, + { "application/ld+json", ContentType.Jsonld }, + { "application/jsonml+json", ContentType.Jsonml }, + { "text/jsx", ContentType.Jsx }, + { "image/jxr", ContentType.Jxr }, + { "image/jxra", ContentType.Jxra }, + { "image/jxrs", ContentType.Jxrs }, + { "image/jxs", ContentType.Jxs }, + { "image/jxsc", ContentType.Jxsc }, + { "image/jxsi", ContentType.Jxsi }, + { "image/jxss", ContentType.Jxss }, + { "application/vnd.kde.karbon", ContentType.Karbon }, + { "application/x-keepass2", ContentType.Kdbx }, + { "application/vnd.apple.keynote", ContentType.Key }, + { "application/vnd.kde.kformula", ContentType.Kfo }, + { "application/vnd.kidspiration", ContentType.Kia }, + { "application/vnd.google-earth.kml+xml", ContentType.Kml }, + { "application/vnd.google-earth.kmz", ContentType.Kmz }, + { "application/vnd.kinar", ContentType.Kne }, + { "application/vnd.kde.kontour", ContentType.Kon }, + { "application/vnd.kde.kpresenter", ContentType.Kpr }, + { "application/vnd.ds-keypoint", ContentType.Kpxx }, + { "application/vnd.kde.kspread", ContentType.Ksp }, + { "image/ktx", ContentType.Ktx }, + { "image/ktx2", ContentType.Ktx2 }, + { "application/vnd.kahootz", ContentType.Ktz }, + { "application/vnd.kde.kword", ContentType.Kwd }, + { "application/vnd.las.las+xml", ContentType.Lasxml }, + { "application/x-latex", ContentType.Latex }, + { "application/vnd.llamagraphics.life-balance.desktop", ContentType.Lbd }, + { "application/vnd.llamagraphics.life-balance.exchange+xml", ContentType.Lbe }, + { "application/vnd.hhe.lesson-player", ContentType.Les }, + { "text/less", ContentType.Less }, + { "application/lgr+xml", ContentType.Lgr }, + { "application/vnd.route66.link66+xml", ContentType.Link66 }, + { "application/x-ms-shortcut", ContentType.Lnk }, + { "application/lost+xml", ContentType.Lostxml }, + { "application/vnd.ms-lrm", ContentType.Lrm }, + { "application/vnd.frogans.ltf", ContentType.Ltf }, + { "text/x-lua", ContentType.Lua }, + { "application/x-lua-bytecode", ContentType.Luac }, + { "audio/vnd.lucent.voice", ContentType.Lvp }, + { "application/vnd.lotus-wordpro", ContentType.Lwp }, + { "application/x-lzh-compressed", ContentType.Lzh }, + { "audio/mpeg", ContentType.M2a }, + { "audio/x-mpegurl", ContentType.M3u }, + { "application/vnd.apple.mpegurl", ContentType.M3u8 }, + { "audio/mp4", ContentType.M4a }, + { "video/iso.segment", ContentType.M4s }, + { "video/vnd.mpegurl", ContentType.M4u }, + { "video/x-m4v", ContentType.M4v }, + { "application/mathematica", ContentType.Ma }, + { "application/mads+xml", ContentType.Mads }, + { "application/mmt-aei+xml", ContentType.Maei }, + { "application/vnd.ecowin.chart", ContentType.Mag }, + { "text/cache-manifest", ContentType.Manifest }, + { "text/markdown", ContentType.Markdown }, + { "application/mathml+xml", ContentType.Mathml }, + { "application/vnd.mobius.mbk", ContentType.Mbk }, + { "application/mbox", ContentType.Mbox }, + { "application/vnd.medcalcdata", ContentType.Mc1 }, + { "application/vnd.mcd", ContentType.Mcd }, + { "text/vnd.curl.mcurl", ContentType.Mcurl }, + { "application/x-msaccess", ContentType.Mdb }, + { "image/vnd.ms-modi", ContentType.Mdi }, + { "text/mdx", ContentType.Mdx }, + { "model/mesh", ContentType.Mesh }, + { "application/metalink4+xml", ContentType.Meta4 }, + { "application/metalink+xml", ContentType.Metalink }, + { "application/mets+xml", ContentType.Mets }, + { "application/vnd.mfmp", ContentType.Mfm }, + { "application/rpki-manifest", ContentType.Mft }, + { "application/vnd.osgeo.mapguide.package", ContentType.Mgp }, + { "application/vnd.proteus.magazine", ContentType.Mgz }, + { "audio/midi", ContentType.Midi }, + { "application/x-mie", ContentType.Mie }, + { "application/vnd.mif", ContentType.Mif }, + { "message/rfc822", ContentType.Mime }, + { "video/mj2", ContentType.Mj2 }, + { "audio/x-matroska", ContentType.Mka }, + { "text/x-markdown", ContentType.Mkd }, + { "video/x-matroska", ContentType.Mkv }, + { "application/vnd.dolby.mlp", ContentType.Mlp }, + { "application/vnd.chipnuts.karaoke-mmd", ContentType.Mmd }, + { "application/vnd.smaf", ContentType.Mmf }, + { "text/mathml", ContentType.Mml }, + { "image/vnd.fujixerox.edmics-mmr", ContentType.Mmr }, + { "video/x-mng", ContentType.Mng }, + { "application/x-msmoney", ContentType.Mny }, + { "application/mods+xml", ContentType.Mods }, + { "video/x-sgi-movie", ContentType.Movie }, + { "application/mp21", ContentType.Mp21 }, + { "audio/mp3", ContentType.Mp3 }, + { "video/mp4", ContentType.Mp4 }, + { "application/mp4", ContentType.Mp4s }, + { "application/vnd.mophun.certificate", ContentType.Mpc }, + { "application/dash+xml", ContentType.Mpd }, + { "video/mpeg", ContentType.Mpeg }, + { "application/vnd.apple.installer+xml", ContentType.Mpkg }, + { "application/vnd.blueice.multipass", ContentType.Mpm }, + { "application/vnd.mophun.application", ContentType.Mpn }, + { "application/vnd.ms-project", ContentType.Mpt }, + { "application/vnd.ibm.minipay", ContentType.Mpy }, + { "application/vnd.mobius.mqy", ContentType.Mqy }, + { "application/marc", ContentType.Mrc }, + { "application/marcxml+xml", ContentType.Mrcx }, + { "application/mediaservercontrol+xml", ContentType.Mscml }, + { "application/vnd.fdsn.mseed", ContentType.Mseed }, + { "application/vnd.mseq", ContentType.Mseq }, + { "application/vnd.epson.msf", ContentType.Msf }, + { "application/vnd.ms-outlook", ContentType.Msg }, + { "application/vnd.mobius.msl", ContentType.Msl }, + { "application/vnd.muvee.style", ContentType.Msty }, + { "model/mtl", ContentType.Mtl }, + { "model/vnd.mts", ContentType.Mts }, + { "application/vnd.musician", ContentType.Mus }, + { "application/mmt-usd+xml", ContentType.Musd }, + { "application/vnd.recordare.musicxml+xml", ContentType.Musicxml }, + { "application/x-msmediaview", ContentType.Mvb }, + { "application/vnd.mapbox-vector-tile", ContentType.Mvt }, + { "application/vnd.mfer", ContentType.Mwf }, + { "application/mxf", ContentType.Mxf }, + { "application/vnd.recordare.musicxml", ContentType.Mxl }, + { "audio/mobile-xmf", ContentType.Mxmf }, + { "application/vnd.triscape.mxs", ContentType.Mxs }, + { "text/n3", ContentType.N3 }, + { "application/vnd.wolfram.player", ContentType.Nbp }, + { "application/x-dtbncx+xml", ContentType.Ncx }, + { "text/x-nfo", ContentType.Nfo }, + { "application/vnd.nokia.n-gage.data", ContentType.Ngdat }, + { "application/vnd.nitf", ContentType.Nitf }, + { "application/vnd.neurolanguage.nlu", ContentType.Nlu }, + { "application/vnd.enliven", ContentType.Nml }, + { "application/vnd.noblenet-directory", ContentType.Nnd }, + { "application/vnd.noblenet-sealer", ContentType.Nns }, + { "application/vnd.noblenet-web", ContentType.Nnw }, + { "image/vnd.net-fpx", ContentType.Npx }, + { "application/n-quads", ContentType.Nq }, + { "application/x-conference", ContentType.Nsc }, + { "application/vnd.lotus-notes", ContentType.Nsf }, + { "application/n-triples", ContentType.Nt }, + { "application/vnd.apple.numbers", ContentType.Numbers }, + { "application/x-nzb", ContentType.Nzb }, + { "application/vnd.fujitsu.oasys2", ContentType.Oa2 }, + { "application/vnd.fujitsu.oasys3", ContentType.Oa3 }, + { "application/vnd.fujitsu.oasys", ContentType.Oas }, + { "application/x-msbinder", ContentType.Obd }, + { "application/vnd.openblox.game+xml", ContentType.Obgx }, + { "application/x-tgif", ContentType.Obj }, + { "application/oda", ContentType.Oda }, + { "application/vnd.oasis.opendocument.database", ContentType.Odb }, + { "application/vnd.oasis.opendocument.chart", ContentType.Odc }, + { "application/vnd.oasis.opendocument.formula", ContentType.Odf }, + { "application/vnd.oasis.opendocument.formula-template", ContentType.Odft }, + { "application/vnd.oasis.opendocument.graphics", ContentType.Odg }, + { "application/vnd.oasis.opendocument.image", ContentType.Odi }, + { "application/vnd.oasis.opendocument.text-master", ContentType.Odm }, + { "application/vnd.oasis.opendocument.presentation", ContentType.Odp }, + { "application/vnd.oasis.opendocument.spreadsheet", ContentType.Ods }, + { "application/vnd.oasis.opendocument.text", ContentType.Odt }, + { "model/vnd.opengex", ContentType.Ogex }, + { "audio/ogg", ContentType.Ogg }, + { "video/ogg", ContentType.Ogv }, + { "application/ogg", ContentType.Ogx }, + { "application/omdoc+xml", ContentType.Omdoc }, + { "application/onenote", ContentType.Onetoc }, + { "application/oebps-package+xml", ContentType.Opf }, + { "text/x-opml", ContentType.Opml }, + { "application/vnd.lotus-organizer", ContentType.Org }, + { "application/vnd.yamaha.openscoreformat", ContentType.Osf }, + { "application/vnd.yamaha.openscoreformat.osfpvg+xml", ContentType.Osfpvg }, + { "application/vnd.openstreetmap.data+xml", ContentType.Osm }, + { "application/vnd.oasis.opendocument.chart-template", ContentType.Otc }, + { "font/otf", ContentType.Otf }, + { "application/vnd.oasis.opendocument.graphics-template", ContentType.Otg }, + { "application/vnd.oasis.opendocument.text-web", ContentType.Oth }, + { "application/vnd.oasis.opendocument.image-template", ContentType.Oti }, + { "application/vnd.oasis.opendocument.presentation-template", ContentType.Otp }, + { "application/vnd.oasis.opendocument.spreadsheet-template", ContentType.Ots }, + { "application/vnd.oasis.opendocument.text-template", ContentType.Ott }, + { "application/x-virtualbox-ova", ContentType.Ova }, + { "application/x-virtualbox-ovf", ContentType.Ovf }, + { "application/oxps", ContentType.Oxps }, + { "application/vnd.openofficeorg.extension", ContentType.Oxt }, + { "text/x-pascal", ContentType.P }, + { "application/pkcs10", ContentType.P10 }, + { "application/x-pkcs7-certificates", ContentType.P7b }, + { "application/pkcs7-mime", ContentType.P7m }, + { "application/x-pkcs7-certreqresp", ContentType.P7r }, + { "application/pkcs7-signature", ContentType.P7s }, + { "application/pkcs8", ContentType.P8 }, + { "application/x-ns-proxy-autoconfig", ContentType.Pac }, + { "application/vnd.apple.pages", ContentType.Pages }, + { "application/vnd.pawaafile", ContentType.Paw }, + { "application/vnd.powerbuilder6", ContentType.Pbd }, + { "image/x-portable-bitmap", ContentType.Pbm }, + { "application/vnd.tcpdump.pcap", ContentType.Pcap }, + { "application/x-font-pcf", ContentType.Pcf }, + { "application/vnd.hp-pcl", ContentType.Pcl }, + { "application/vnd.hp-pclxl", ContentType.Pclxl }, + { "image/x-pict", ContentType.Pct }, + { "application/vnd.curl.pcurl", ContentType.Pcurl }, + { "image/vnd.zbrush.pcx", ContentType.Pcx }, + { "application/vnd.palm", ContentType.Pdb }, + { "text/x-processing", ContentType.Pde }, + { "application/pdf", ContentType.Pdf }, + { "application/x-font-type1", ContentType.Pfa }, + { "application/font-tdpfr", ContentType.Pfr }, + { "application/x-pkcs12", ContentType.Pfx }, + { "image/x-portable-graymap", ContentType.Pgm }, + { "application/x-chess-pgn", ContentType.Pgn }, + { "application/pgp-encrypted", ContentType.Pgp }, + { "application/x-httpd-php", ContentType.Php }, + { "application/pkixcmp", ContentType.Pki }, + { "application/pkix-pkipath", ContentType.Pkipath }, + { "application/vnd.apple.pkpass", ContentType.Pkpass }, + { "application/x-perl", ContentType.Pl }, + { "application/vnd.3gpp.pic-bw-large", ContentType.Plb }, + { "application/vnd.mobius.plc", ContentType.Plc }, + { "application/vnd.pocketlearn", ContentType.Plf }, + { "application/pls+xml", ContentType.Pls }, + { "application/vnd.ctc-posml", ContentType.Pml }, + { "image/png", ContentType.Png }, + { "image/x-portable-anymap", ContentType.Pnm }, + { "application/vnd.macports.portpkg", ContentType.Portpkg }, + { "application/vnd.ms-powerpoint.template.macroenabled.12", ContentType.Potm }, + { "application/vnd.openxmlformats-officedocument.presentationml.template", ContentType.Potx }, + { "application/vnd.ms-powerpoint.addin.macroenabled.12", ContentType.Ppam }, + { "application/vnd.cups-ppd", ContentType.Ppd }, + { "image/x-portable-pixmap", ContentType.Ppm }, + { "application/vnd.ms-powerpoint.slideshow.macroenabled.12", ContentType.Ppsm }, + { "application/vnd.openxmlformats-officedocument.presentationml.slideshow", ContentType.Ppsx }, + { "application/vnd.ms-powerpoint", ContentType.Ppt }, + { "application/vnd.ms-powerpoint.presentation.macroenabled.12", ContentType.Pptm }, + { "application/vnd.openxmlformats-officedocument.presentationml.presentation", ContentType.Pptx }, + { "application/x-mobipocket-ebook", ContentType.Prc }, + { "application/vnd.lotus-freelance", ContentType.Pre }, + { "application/pics-rules", ContentType.Prf }, + { "application/provenance+xml", ContentType.Provx }, + { "application/postscript", ContentType.Ps }, + { "application/vnd.3gpp.pic-bw-small", ContentType.Psb }, + { "image/vnd.adobe.photoshop", ContentType.Psd }, + { "application/x-font-linux-psf", ContentType.Psf }, + { "application/pskc+xml", ContentType.Pskcxml }, + { "image/prs.pti", ContentType.Pti }, + { "application/vnd.pvi.ptid1", ContentType.Ptid }, + { "application/x-mspublisher", ContentType.Pub }, + { "application/vnd.3gpp.pic-bw-var", ContentType.Pvb }, + { "application/vnd.3m.post-it-notes", ContentType.Pwn }, + { "audio/vnd.ms-playready.media.pya", ContentType.Pya }, + { "video/vnd.ms-playready.media.pyv", ContentType.Pyv }, + { "application/vnd.epson.quickanime", ContentType.Qam }, + { "application/vnd.intu.qbo", ContentType.Qbo }, + { "application/vnd.intu.qfx", ContentType.Qfx }, + { "application/vnd.publishare-delta-tree", ContentType.Qps }, + { "video/quicktime", ContentType.Qt }, + { "application/vnd.quark.quarkxpress", ContentType.Qwd }, + { "audio/x-pn-realaudio", ContentType.Ra }, + { "application/raml+yaml", ContentType.Raml }, + { "application/route-apd+xml", ContentType.Rapd }, + { "application/vnd.rar", ContentType.Rar }, + { "image/x-cmu-raster", ContentType.Ras }, + { "application/rdf+xml", ContentType.Rdf }, + { "application/vnd.data-vision.rdz", ContentType.Rdz }, + { "application/p2p-overlay+xml", ContentType.Relo }, + { "application/vnd.businessobjects", ContentType.Rep }, + { "application/x-dtbresource+xml", ContentType.Res }, + { "image/x-rgb", ContentType.Rgb }, + { "application/reginfo+xml", ContentType.Rif }, + { "audio/vnd.rip", ContentType.Rip }, + { "application/x-research-info-systems", ContentType.Ris }, + { "application/resource-lists+xml", ContentType.Rl }, + { "image/vnd.fujixerox.edmics-rlc", ContentType.Rlc }, + { "application/resource-lists-diff+xml", ContentType.Rld }, + { "application/vnd.rn-realmedia", ContentType.Rm }, + { "audio/x-pn-realaudio-plugin", ContentType.Rmp }, + { "application/vnd.jcp.javame.midlet-rms", ContentType.Rms }, + { "application/vnd.rn-realmedia-vbr", ContentType.Rmvb }, + { "application/relax-ng-compact-syntax", ContentType.Rnc }, + { "application/rpki-roa", ContentType.Roa }, + { "text/troff", ContentType.Roff }, + { "application/vnd.cloanto.rp9", ContentType.Rp9 }, + { "application/x-redhat-package-manager", ContentType.Rpm }, + { "application/vnd.nokia.radio-presets", ContentType.Rpss }, + { "application/vnd.nokia.radio-preset", ContentType.Rpst }, + { "application/sparql-query", ContentType.Rq }, + { "application/rls-services+xml", ContentType.Rs }, + { "application/atsc-rsat+xml", ContentType.Rsat }, + { "application/rsd+xml", ContentType.Rsd }, + { "application/urc-ressheet+xml", ContentType.Rsheet }, + { "application/rss+xml", ContentType.Rss }, + { "application/rtf", ContentType.Rtf }, + { "text/richtext", ContentType.Rtx }, + { "application/x-makeself", ContentType.Run }, + { "application/route-usd+xml", ContentType.Rusd }, + { "audio/s3m", ContentType.S3m }, + { "application/vnd.yamaha.smaf-audio", ContentType.Saf }, + { "text/x-sass", ContentType.Sass }, + { "application/sbml+xml", ContentType.Sbml }, + { "application/vnd.ibm.secure-container", ContentType.Sc }, + { "application/x-msschedule", ContentType.Scd }, + { "application/vnd.lotus-screencam", ContentType.Scm }, + { "application/scvp-cv-request", ContentType.Scq }, + { "application/scvp-cv-response", ContentType.Scs }, + { "text/x-scss", ContentType.Scss }, + { "text/vnd.curl.scurl", ContentType.Scurl }, + { "application/vnd.stardivision.draw", ContentType.Sda }, + { "application/vnd.stardivision.calc", ContentType.Sdc }, + { "application/vnd.stardivision.impress", ContentType.Sdd }, + { "application/vnd.solent.sdkm+xml", ContentType.Sdkm }, + { "application/sdp", ContentType.Sdp }, + { "application/vnd.stardivision.writer", ContentType.Sdw }, + { "application/x-sea", ContentType.Sea }, + { "application/vnd.seemail", ContentType.See }, + { "application/vnd.fdsn.seed", ContentType.Seed }, + { "application/vnd.sema", ContentType.Sema }, + { "application/vnd.semd", ContentType.Semd }, + { "application/vnd.semf", ContentType.Semf }, + { "application/senml+xml", ContentType.Senmlx }, + { "application/sensml+xml", ContentType.Sensmlx }, + { "application/java-serialized-object", ContentType.Ser }, + { "application/set-payment-initiation", ContentType.Setpay }, + { "application/set-registration-initiation", ContentType.Setreg }, + { "application/vnd.spotfire.sfs", ContentType.Sfs }, + { "text/x-sfv", ContentType.Sfv }, + { "image/sgi", ContentType.Sgi }, + { "application/vnd.stardivision.writer-global", ContentType.Sgl }, + { "text/sgml", ContentType.Sgml }, + { "application/x-sh", ContentType.Sh }, + { "application/x-shar", ContentType.Shar }, + { "text/shex", ContentType.Shex }, + { "application/shf+xml", ContentType.Shf }, + { "image/x-mrsid-image", ContentType.Sid }, + { "application/sieve", ContentType.Sieve }, + { "application/pgp-signature", ContentType.Sig }, + { "audio/silk", ContentType.Sil }, + { "application/vnd.symbian.install", ContentType.Sisx }, + { "application/x-stuffit", ContentType.Sit }, + { "application/x-stuffitx", ContentType.Sitx }, + { "application/vnd.koan", ContentType.Skd }, + { "application/vnd.ms-powerpoint.slide.macroenabled.12", ContentType.Sldm }, + { "application/vnd.openxmlformats-officedocument.presentationml.slide", ContentType.Sldx }, + { "text/slim", ContentType.Slim }, + { "application/route-s-tsid+xml", ContentType.Sls }, + { "application/vnd.epson.salt", ContentType.Slt }, + { "application/vnd.stepmania.stepchart", ContentType.Sm }, + { "application/vnd.stardivision.math", ContentType.Smf }, + { "application/smil+xml", ContentType.Smil }, + { "video/x-smv", ContentType.Smv }, + { "application/vnd.stepmania.package", ContentType.Smzip }, + { "application/x-font-snf", ContentType.Snf }, + { "text/spdx", ContentType.Spdx }, + { "application/vnd.yamaha.smaf-phrase", ContentType.Spf }, + { "application/x-futuresplash", ContentType.Spl }, + { "text/vnd.in3d.spot", ContentType.Spot }, + { "application/scvp-vp-response", ContentType.Spp }, + { "application/scvp-vp-request", ContentType.Spq }, + { "application/x-sql", ContentType.Sql }, + { "application/x-wais-source", ContentType.Src }, + { "application/x-subrip", ContentType.Srt }, + { "application/sru+xml", ContentType.Sru }, + { "application/sparql-results+xml", ContentType.Srx }, + { "application/ssdl+xml", ContentType.Ssdl }, + { "application/vnd.kodak-descriptor", ContentType.Sse }, + { "application/vnd.epson.ssf", ContentType.Ssf }, + { "application/ssml+xml", ContentType.Ssml }, + { "application/vnd.sailingtracker.track", ContentType.St }, + { "application/vnd.sun.xml.calc.template", ContentType.Stc }, + { "application/vnd.sun.xml.draw.template", ContentType.Std }, + { "application/vnd.wt.stf", ContentType.Stf }, + { "application/vnd.sun.xml.impress.template", ContentType.Sti }, + { "application/hyperstudio", ContentType.Stk }, + { "application/vnd.ms-pki.stl", ContentType.Stl }, + { "model/step+xml", ContentType.Stpx }, + { "model/step-xml+zip", ContentType.Stpxz }, + { "model/step+zip", ContentType.Stpz }, + { "application/vnd.pg.format", ContentType.Str }, + { "application/vnd.sun.xml.writer.template", ContentType.Stw }, + { "text/stylus", ContentType.Stylus }, + { "image/vnd.dvb.subtitle", ContentType.Sub }, + { "application/vnd.sus-calendar", ContentType.Sus }, + { "application/x-sv4cpio", ContentType.Sv4cpio }, + { "application/x-sv4crc", ContentType.Sv4crc }, + { "application/vnd.dvb.service", ContentType.Svc }, + { "application/vnd.svd", ContentType.Svd }, + { "image/svg+xml", ContentType.Svg }, + { "application/x-shockwave-flash", ContentType.Swf }, + { "application/vnd.aristanetworks.swi", ContentType.Swi }, + { "application/swid+xml", ContentType.Swidtag }, + { "application/vnd.sun.xml.calc", ContentType.Sxc }, + { "application/vnd.sun.xml.draw", ContentType.Sxd }, + { "application/vnd.sun.xml.writer.global", ContentType.Sxg }, + { "application/vnd.sun.xml.impress", ContentType.Sxi }, + { "application/vnd.sun.xml.math", ContentType.Sxm }, + { "application/vnd.sun.xml.writer", ContentType.Sxw }, + { "application/x-t3vm-image", ContentType.T3 }, + { "image/t38", ContentType.T38 }, + { "application/vnd.mynfc", ContentType.Taglet }, + { "application/vnd.tao.intent-module-archive", ContentType.Tao }, + { "image/vnd.tencent.tap", ContentType.Tap }, + { "application/x-tar", ContentType.Tar }, + { "application/vnd.3gpp2.tcap", ContentType.Tcap }, + { "application/x-tcl", ContentType.Tcl }, + { "application/urc-targetdesc+xml", ContentType.Td }, + { "application/vnd.smart.teacher", ContentType.Teacher }, + { "application/tei+xml", ContentType.Tei }, + { "application/x-tex", ContentType.Tex }, + { "application/x-texinfo", ContentType.Texinfo }, + { "text/plain", ContentType.Text }, + { "application/thraud+xml", ContentType.Tfi }, + { "application/x-tex-tfm", ContentType.Tfm }, + { "image/tiff-fx", ContentType.Tfx }, + { "image/x-tga", ContentType.Tga }, + { "application/vnd.ms-officetheme", ContentType.Thmx }, + { "image/tiff", ContentType.Tiff }, + { "application/vnd.tmobile-livetv", ContentType.Tmo }, + { "application/toml", ContentType.Toml }, + { "application/x-bittorrent", ContentType.Torrent }, + { "application/vnd.groove-tool-template", ContentType.Tpl }, + { "application/vnd.trid.tpt", ContentType.Tpt }, + { "application/vnd.trueapp", ContentType.Tra }, + { "application/trig", ContentType.Trig }, + { "application/x-msterminal", ContentType.Trm }, + { "video/mp2t", ContentType.Ts }, + { "application/timestamped-data", ContentType.Tsd }, + { "text/tab-separated-values", ContentType.Tsv }, + { "font/collection", ContentType.Ttc }, + { "font/ttf", ContentType.Ttf }, + { "text/turtle", ContentType.Ttl }, + { "application/ttml+xml", ContentType.Ttml }, + { "application/vnd.simtech-mindmapper", ContentType.Twd }, + { "application/vnd.genomatix.tuxedo", ContentType.Txd }, + { "application/vnd.mobius.txf", ContentType.Txf }, + { "application/x-authorware-bin", ContentType.U32 }, + { "message/global-delivery-status", ContentType.U8dsn }, + { "message/global-headers", ContentType.U8hdr }, + { "message/global-disposition-notification", ContentType.U8mdn }, + { "message/global", ContentType.U8msg }, + { "application/ubjson", ContentType.Ubj }, + { "application/x-debian-package", ContentType.Udeb }, + { "application/vnd.ufdl", ContentType.Ufdl }, + { "application/x-glulx", ContentType.Ulx }, + { "application/vnd.umajin", ContentType.Umj }, + { "application/vnd.unity", ContentType.Unityweb }, + { "application/vnd.uoml+xml", ContentType.Uoml }, + { "text/uri-list", ContentType.Uri }, + { "model/vnd.usdz+zip", ContentType.Usdz }, + { "application/x-ustar", ContentType.Ustar }, + { "application/vnd.uiq.theme", ContentType.Utz }, + { "text/x-uuencode", ContentType.Uu }, + { "audio/vnd.dece.audio", ContentType.Uva }, + { "application/vnd.dece.data", ContentType.Uvd }, + { "image/vnd.dece.graphic", ContentType.Uvg }, + { "video/vnd.dece.hd", ContentType.Uvh }, + { "video/vnd.dece.mobile", ContentType.Uvm }, + { "video/vnd.dece.pd", ContentType.Uvp }, + { "video/vnd.dece.sd", ContentType.Uvs }, + { "application/vnd.dece.ttml+xml", ContentType.Uvt }, + { "video/vnd.uvvu.mp4", ContentType.Uvu }, + { "video/vnd.dece.video", ContentType.Uvv }, + { "application/vnd.dece.zip", ContentType.Uvz }, + { "application/x-virtualbox-vbox", ContentType.Vbox }, + { "text/vcard", ContentType.Vcard }, + { "application/x-cdlink", ContentType.Vcd }, + { "text/x-vcard", ContentType.Vcf }, + { "application/vnd.groove-vcard", ContentType.Vcg }, + { "text/x-vcalendar", ContentType.Vcs }, + { "application/vnd.vcx", ContentType.Vcx }, + { "application/x-virtualbox-vdi", ContentType.Vdi }, + { "model/vnd.sap.vds", ContentType.Vds }, + { "application/x-virtualbox-vhd", ContentType.Vhd }, + { "application/vnd.visionary", ContentType.Vis }, + { "video/vnd.vivo", ContentType.Viv }, + { "application/x-virtualbox-vmdk", ContentType.Vmdk }, + { "video/x-ms-vob", ContentType.Vob }, + { "model/vrml", ContentType.Vrml }, + { "application/vnd.vsf", ContentType.Vsf }, + { "application/vnd.visio", ContentType.Vss }, + { "image/vnd.valve.source.texture", ContentType.Vtf }, + { "text/vtt", ContentType.Vtt }, + { "model/vnd.vtu", ContentType.Vtu }, + { "application/voicexml+xml", ContentType.Vxml }, + { "application/x-doom", ContentType.Wad }, + { "application/vnd.sun.wadl+xml", ContentType.Wadl }, + { "application/wasm", ContentType.Wasm }, + { "audio/wav", ContentType.Wav }, + { "audio/x-ms-wax", ContentType.Wax }, + { "image/vnd.wap.wbmp", ContentType.Wbmp }, + { "application/vnd.criticaltools.wbs+xml", ContentType.Wbs }, + { "application/vnd.wap.wbxml", ContentType.Wbxml }, + { "image/vnd.ms-photo", ContentType.Wdp }, + { "audio/webm", ContentType.Weba }, + { "application/x-web-app-manifest+json", ContentType.Webapp }, + { "video/webm", ContentType.Webm }, + { "image/webp", ContentType.Webp }, + { "application/vnd.pmi.widget", ContentType.Wg }, + { "application/widget", ContentType.Wgt }, + { "application/vnd.ms-works", ContentType.Wks }, + { "video/x-ms-wm", ContentType.Wm }, + { "audio/x-ms-wma", ContentType.Wma }, + { "application/x-ms-wmd", ContentType.Wmd }, + { "application/x-msmetafile", ContentType.Wmf }, + { "text/vnd.wap.wml", ContentType.Wml }, + { "application/vnd.wap.wmlc", ContentType.Wmlc }, + { "text/vnd.wap.wmlscript", ContentType.Wmls }, + { "application/vnd.wap.wmlscriptc", ContentType.Wmlsc }, + { "video/x-ms-wmv", ContentType.Wmv }, + { "video/x-ms-wmx", ContentType.Wmx }, + { "application/x-ms-wmz", ContentType.Wmz }, + { "font/woff", ContentType.Woff }, + { "font/woff2", ContentType.Woff2 }, + { "application/vnd.wordperfect", ContentType.Wpd }, + { "application/vnd.ms-wpl", ContentType.Wpl }, + { "application/vnd.wqd", ContentType.Wqd }, + { "application/x-mswrite", ContentType.Wri }, + { "message/vnd.wfa.wsc", ContentType.Wsc }, + { "application/wsdl+xml", ContentType.Wsdl }, + { "application/wspolicy+xml", ContentType.Wspolicy }, + { "application/vnd.webturbo", ContentType.Wtb }, + { "video/x-ms-wvx", ContentType.Wvx }, + { "model/x3d+xml", ContentType.X3d }, + { "model/x3d+binary", ContentType.X3db }, + { "model/x3d+vrml", ContentType.X3dv }, + { "application/xaml+xml", ContentType.Xaml }, + { "application/x-silverlight-app", ContentType.Xap }, + { "application/vnd.xara", ContentType.Xar }, + { "application/xcap-att+xml", ContentType.Xav }, + { "application/x-ms-xbap", ContentType.Xbap }, + { "application/vnd.fujixerox.docuworks.binder", ContentType.Xbd }, + { "image/x-xbitmap", ContentType.Xbm }, + { "application/xcap-caps+xml", ContentType.Xca }, + { "application/calendar+xml", ContentType.Xcs }, + { "application/xcap-diff+xml", ContentType.Xdf }, + { "application/vnd.syncml.dm+xml", ContentType.Xdm }, + { "application/vnd.adobe.xdp+xml", ContentType.Xdp }, + { "application/dssc+xml", ContentType.Xdssc }, + { "application/vnd.fujixerox.docuworks", ContentType.Xdw }, + { "application/xcap-el+xml", ContentType.Xel }, + { "application/xenc+xml", ContentType.Xenc }, + { "application/patch-ops-error+xml", ContentType.Xer }, + { "application/vnd.adobe.xfdf", ContentType.Xfdf }, + { "application/vnd.xfdl", ContentType.Xfdl }, + { "application/xhtml+xml", ContentType.Xhtml }, + { "image/vnd.xiff", ContentType.Xif }, + { "application/vnd.ms-excel.addin.macroenabled.12", ContentType.Xlam }, + { "application/x-xliff+xml", ContentType.Xlf }, + { "application/vnd.ms-excel", ContentType.Xls }, + { "application/vnd.ms-excel.sheet.binary.macroenabled.12", ContentType.Xlsb }, + { "application/vnd.ms-excel.sheet.macroenabled.12", ContentType.Xlsm }, + { "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ContentType.Xlsx }, + { "application/vnd.ms-excel.template.macroenabled.12", ContentType.Xltm }, + { "application/vnd.openxmlformats-officedocument.spreadsheetml.template", ContentType.Xltx }, + { "audio/xm", ContentType.Xm }, + { "application/xml", ContentType.Xml }, + { "application/xcap-ns+xml", ContentType.Xns }, + { "application/vnd.olpc-sugar", ContentType.Xo }, + { "application/xop+xml", ContentType.Xop }, + { "application/x-xpinstall", ContentType.Xpi }, + { "application/xproc+xml", ContentType.Xpl }, + { "image/x-xpixmap", ContentType.Xpm }, + { "application/vnd.is-xpr", ContentType.Xpr }, + { "application/vnd.ms-xpsdocument", ContentType.Xps }, + { "application/vnd.intercon.formnet", ContentType.Xpw }, + { "application/xslt+xml", ContentType.Xslt }, + { "application/vnd.syncml+xml", ContentType.Xsm }, + { "application/vnd.mozilla.xul+xml", ContentType.Xul }, + { "application/xv+xml", ContentType.Xvml }, + { "image/x-xwindowdump", ContentType.Xwd }, + { "chemical/x-xyz", ContentType.Xyz }, + { "application/x-xz", ContentType.Xz }, + { "text/yaml", ContentType.Yaml }, + { "application/yang", ContentType.Yang }, + { "application/yin+xml", ContentType.Yin }, + { "text/x-suse-ymp", ContentType.Ymp }, + { "application/x-zmachine", ContentType.Z1 }, + { "application/vnd.zzazz.deck+xml", ContentType.Zaz }, + { "application/zip", ContentType.Zip }, + { "application/vnd.zul", ContentType.Zir }, + { "application/vnd.handheld-entertainment+xml", ContentType.Zmm }, + }; + } +} diff --git a/Net.Http/src/Helpers/TransportReader.cs b/Net.Http/src/Helpers/TransportReader.cs new file mode 100644 index 0000000..a37bfe9 --- /dev/null +++ b/Net.Http/src/Helpers/TransportReader.cs @@ -0,0 +1,114 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: TransportReader.cs +* +* TransportReader.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Text; + +using VNLib.Utils; +using VNLib.Utils.IO; + + +namespace VNLib.Net.Http.Core +{ + + /// <summary> + /// Structure implementation of <see cref="IVnTextReader"/> + /// </summary> + internal struct TransportReader : IVnTextReader + { + ///<inheritdoc/> + public readonly Encoding Encoding => _encoding; + ///<inheritdoc/> + public readonly ReadOnlyMemory<byte> LineTermination => _lineTermination; + ///<inheritdoc/> + public readonly Stream BaseStream => _transport; + + + private readonly SharedHeaderReaderBuffer BinBuffer; + private readonly Encoding _encoding; + private readonly Stream _transport; + private readonly ReadOnlyMemory<byte> _lineTermination; + + private int BufWindowStart; + private int BufWindowEnd; + + /// <summary> + /// Initializes a new <see cref="TransportReader"/> for reading text lines from the transport stream + /// </summary> + /// <param name="transport">The transport stream to read data from</param> + /// <param name="buffer">The shared binary buffer</param> + /// <param name="encoding">The encoding to use when reading bianry</param> + /// <param name="lineTermination">The line delimiter to search for</param> + public TransportReader(Stream transport, SharedHeaderReaderBuffer buffer, Encoding encoding, ReadOnlyMemory<byte> lineTermination) + { + BufWindowEnd = 0; + BufWindowStart = 0; + _encoding = encoding; + _transport = transport; + _lineTermination = lineTermination; + BinBuffer = buffer; + } + + ///<inheritdoc/> + public readonly int Available => BufWindowEnd - BufWindowStart; + + ///<inheritdoc/> + public readonly Span<byte> BufferedDataWindow => BinBuffer.BinBuffer[BufWindowStart..BufWindowEnd]; + + + ///<inheritdoc/> + public void Advance(int count) => BufWindowStart += count; + ///<inheritdoc/> + public void FillBuffer() + { + //Get a buffer from the end of the current window to the end of the buffer + Span<byte> bufferWindow = BinBuffer.BinBuffer[BufWindowEnd..]; + //Read from stream + int read = _transport.Read(bufferWindow); + //Update the end of the buffer window to the end of the read data + BufWindowEnd += read; + } + ///<inheritdoc/> + public ERRNO CompactBufferWindow() + { + //No data to compact if window is not shifted away from start + if (BufWindowStart > 0) + { + //Get span over engire buffer + Span<byte> buffer = BinBuffer.BinBuffer; + //Get data within window + Span<byte> usedData = buffer[BufWindowStart..BufWindowEnd]; + //Copy remaining to the begining of the buffer + usedData.CopyTo(buffer); + //Buffer window start is 0 + BufWindowStart = 0; + //Buffer window end is now the remaining size + BufWindowEnd = usedData.Length; + } + //Return the number of bytes of available space + return BinBuffer.BinLength - BufWindowEnd; + } + } +} diff --git a/Net.Http/src/Helpers/VnWebHeaderCollection.cs b/Net.Http/src/Helpers/VnWebHeaderCollection.cs new file mode 100644 index 0000000..f67e7db --- /dev/null +++ b/Net.Http/src/Helpers/VnWebHeaderCollection.cs @@ -0,0 +1,43 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: VnWebHeaderCollection.cs +* +* VnWebHeaderCollection.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Net; +using System.Collections.Generic; + + +namespace VNLib.Net.Http +{ + ///<inheritdoc/> + public sealed class VnWebHeaderCollection : WebHeaderCollection, IEnumerable<KeyValuePair<string?, string?>> + { + IEnumerator<KeyValuePair<string?, string?>> IEnumerable<KeyValuePair<string?, string?>>.GetEnumerator() + { + for (int i = 0; i < Keys.Count; i++) + { + yield return new(Keys[i], Get(i)); + } + } + } +} diff --git a/Net.Http/src/Helpers/WebHeaderExtensions.cs b/Net.Http/src/Helpers/WebHeaderExtensions.cs new file mode 100644 index 0000000..e51bdd5 --- /dev/null +++ b/Net.Http/src/Helpers/WebHeaderExtensions.cs @@ -0,0 +1,60 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: WebHeaderExtensions.cs +* +* WebHeaderExtensions.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Net; +using System.Runtime.CompilerServices; + +namespace VNLib.Net.Http +{ + /// <summary> + /// Extends the <see cref="WebHeaderCollection"/> to provide some check methods + /// </summary> + public static class WebHeaderExtensions + { + /// <summary> + /// Determines if the specified request header has been set in the current header collection + /// </summary> + /// <param name="headers"></param> + /// <param name="header">Header value to check</param> + /// <returns>true if the header was set, false otherwise</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HeaderSet(this WebHeaderCollection headers, HttpRequestHeader header) => !string.IsNullOrWhiteSpace(headers[header]); + /// <summary> + /// Determines if the specified response header has been set in the current header collection + /// </summary> + /// <param name="headers"></param> + /// <param name="header">Header value to check</param> + /// <returns>true if the header was set, false otherwise</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HeaderSet(this WebHeaderCollection headers, HttpResponseHeader header) => !string.IsNullOrWhiteSpace(headers[header]); + /// <summary> + /// Determines if the specified header has been set in the current header collection + /// </summary> + /// <param name="headers"></param> + /// <param name="header">Header value to check</param> + /// <returns>true if the header was set, false otherwise</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HeaderSet(this WebHeaderCollection headers, string header) => !string.IsNullOrWhiteSpace(headers[header]); + } +}
\ No newline at end of file diff --git a/Net.Http/src/HttpConfig.cs b/Net.Http/src/HttpConfig.cs new file mode 100644 index 0000000..8e73176 --- /dev/null +++ b/Net.Http/src/HttpConfig.cs @@ -0,0 +1,154 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: HttpConfig.cs +* +* HttpConfig.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Text; +using System.IO.Compression; + +using VNLib.Utils.Logging; + +namespace VNLib.Net.Http +{ + /// <summary> + /// Represents configration variables used to create the instance and manage http connections + /// </summary> + public readonly struct HttpConfig + { + public HttpConfig(ILogProvider log) + { + ConnectionKeepAlive = TimeSpan.FromSeconds(100); + ServerLog = log; + } + + /// <summary> + /// A log provider that all server related log entiries will be written to + /// </summary> + public readonly ILogProvider ServerLog { get; } + /// <summary> + /// The absolute request entity body size limit in bytes + /// </summary> + public readonly int MaxUploadSize { get; init; } = 5 * 1000 * 1024; + /// <summary> + /// The maximum size in bytes allowed for an MIME form-data content type upload + /// </summary> + /// <remarks>Set to 0 to disabled mulit-part/form-data uploads</remarks> + public readonly int MaxFormDataUploadSize { get; init; } = 40 * 1024; + /// <summary> + /// The maximum buffer size to use when parsing Multi-part/Form-data file uploads + /// </summary> + /// <remarks> + /// This value is used to create the buffer used to read data from the input stream + /// into memory for parsing. Form-data uploads must be parsed in memory because + /// the data is not delimited by a content length. + /// </remarks> + public readonly int FormDataBufferSize { get; init; } = 8192; + /// <summary> + /// The maximum response entity size in bytes for which the library will allow compresssing response data + /// </summary> + /// <remarks>Set this value to 0 to disable response compression</remarks> + public readonly int CompressionLimit { get; init; } = 1000 * 1024; + /// <summary> + /// The minimum size (in bytes) of respones data that will be compressed + /// </summary> + public readonly int CompressionMinimum { get; init; } = 4096; + /// <summary> + /// The maximum amount of time to listen for data from a connected, but inactive transport connection + /// before closing them + /// </summary> + public readonly TimeSpan ConnectionKeepAlive { get; init; } + /// <summary> + /// The encoding to use when sending and receiving HTTP data + /// </summary> + public readonly Encoding HttpEncoding { get; init; } = Encoding.UTF8; + /// <summary> + /// Sets the compression level for response entity streams of all supported types when + /// compression is used. + /// </summary> + public readonly CompressionLevel CompressionLevel { get; init; } = CompressionLevel.Optimal; + /// <summary> + /// Sets the default Http version for responses when the client version cannot be parsed from the request + /// </summary> + public readonly HttpVersion DefaultHttpVersion { get; init; } = HttpVersion.Http11; + /// <summary> + /// The buffer size used to read HTTP headers from the transport. + /// </summary> + /// <remarks> + /// Setting this value too low will result in header parsing failures + /// and 400 Bad Request responses. Setting it too high can result in + /// resource abuse or high memory usage. 8k is usually a good value. + /// </remarks> + public readonly int HeaderBufferSize { get; init; } = 8192; + /// <summary> + /// The amount of time (in milliseconds) to wait for data on a connection that is in a receive + /// state, aka active receive. + /// </summary> + public readonly int ActiveConnectionRecvTimeout { get; init; } = 5000; + /// <summary> + /// The amount of time (in milliseconds) to wait for data to be send to the client before + /// the connection is closed + /// </summary> + public readonly int SendTimeout { get; init; } = 5000; + /// <summary> + /// The maximum number of request headers allowed per request + /// </summary> + public readonly int MaxRequestHeaderCount { get; init; } = 100; + /// <summary> + /// The maximum number of open transport connections, before 503 errors + /// will be returned and new connections closed. + /// </summary> + /// <remarks>Set to 0 to disable request processing. Causes perminant 503 results</remarks> + public readonly int MaxOpenConnections { get; init; } = int.MaxValue; + /// <summary> + /// The size (in bytes) of the http response header accumulator buffer. + /// </summary> + /// <remarks> + /// Http responses use an internal accumulator to buffer all response headers + /// before writing them to the transport in on write operation. If this value + /// is too low, the response will fail to write. If it is too high, it + /// may cause resource exhaustion or high memory usage. + /// </remarks> + public readonly int ResponseHeaderBufferSize { get; init; } = 16 * 1024; + /// <summary> + /// The size (in bytes) of the buffer to use to discard unread request entity bodies + /// </summary> + public readonly int DiscardBufferSize { get; init; } = 64 * 1024; + /// <summary> + /// The size of the buffer to use when writing response data to the transport + /// </summary> + /// <remarks> + /// This value is the size of the buffer used to copy data from the response + /// entity stream, to the transport stream. + /// </remarks> + public readonly int ResponseBufferSize { get; init; } = 32 * 1024; + /// <summary> + /// The size of the buffer used to accumulate chunked response data before writing to the transport + /// </summary> + public readonly int ChunkedResponseAccumulatorSize { get; init; } = 64 * 1024; + /// <summary> + /// An <see cref="ILogProvider"/> for writing verbose request logs. Set to <c>null</c> + /// to disable verbose request logging + /// </summary> + public readonly ILogProvider? RequestDebugLog { get; init; } = null; + } +}
\ No newline at end of file diff --git a/Net.Http/src/IAlternateProtocol.cs b/Net.Http/src/IAlternateProtocol.cs new file mode 100644 index 0000000..dc7072b --- /dev/null +++ b/Net.Http/src/IAlternateProtocol.cs @@ -0,0 +1,46 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IAlternateProtocol.cs +* +* IAlternateProtocol.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Net.Http +{ + /// <summary> + /// Allows implementation for a protocol swtich from HTTP to another protocol + /// </summary> + public interface IAlternateProtocol + { + /// <summary> + /// Initializes and executes the protocol-switch and the protocol handler + /// that is stored + /// </summary> + /// <param name="transport">The prepared transport stream for the new protocol</param> + /// <param name="handlerToken">A cancelation token that the caller may pass for operation cancelation and cleanup</param> + /// <returns>A task that will be awaited by the server, that when complete, will cleanup resources held by the connection</returns> + Task RunAsync(Stream transport, CancellationToken handlerToken); + } +}
\ No newline at end of file diff --git a/Net.Http/src/IConnectionInfo.cs b/Net.Http/src/IConnectionInfo.cs new file mode 100644 index 0000000..17ee16f --- /dev/null +++ b/Net.Http/src/IConnectionInfo.cs @@ -0,0 +1,150 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IConnectionInfo.cs +* +* IConnectionInfo.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Net; +using System.Text; +using System.Collections.Generic; +using System.Security.Authentication; + +namespace VNLib.Net.Http +{ + /// <summary> + /// Represents a client's connection info as interpreted by the current server + /// </summary> + /// <remarks>Methods and properties are undefined when <see cref="IWebRoot.ClientConnectedAsync(IHttpEvent)"/> returns</remarks> + public interface IConnectionInfo + { + /// <summary> + /// Full request uri of current connection + /// </summary> + Uri RequestUri { get; } + /// <summary> + /// Current request path. Shortcut to <seealso cref="RequestUri"/> <see cref="Uri.LocalPath"/> + /// </summary> + string Path => RequestUri.LocalPath; + /// <summary> + /// Current connection's user-agent header, (may be null if no user-agent header found) + /// </summary> + string? UserAgent { get; } + /// <summary> + /// Current connection's headers + /// </summary> + IHeaderCollection Headers { get; } + /// <summary> + /// A value that indicates if the connection's origin header was set and it's + /// authority segment does not match the <see cref="RequestUri"/> authority + /// segment. + /// </summary> + bool CrossOrigin { get; } + /// <summary> + /// Is the current connecion a websocket request + /// </summary> + bool IsWebSocketRequest { get; } + /// <summary> + /// Request specified content-type + /// </summary> + ContentType ContentType { get; } + /// <summary> + /// Current request's method + /// </summary> + HttpMethod Method { get; } + /// <summary> + /// The current connection's HTTP protocol version + /// </summary> + HttpVersion ProtocolVersion { get; } + /// <summary> + /// Is the connection using transport security? + /// </summary> + bool IsSecure { get; } + /// <summary> + /// The negotiated transport protocol for the current connection + /// </summary> + SslProtocols SecurityProtocol { get; } + /// <summary> + /// Origin header of current connection if specified, null otherwise + /// </summary> + Uri? Origin { get; } + /// <summary> + /// Referer header of current connection if specified, null otherwise + /// </summary> + Uri? Referer { get; } + /// <summary> + /// The parsed range header, or -1,-1 if the range header was not set + /// </summary> + Tuple<long, long>? Range { get; } + /// <summary> + /// The server endpoint that accepted the connection + /// </summary> + IPEndPoint LocalEndpoint { get; } + /// <summary> + /// The raw <see cref="IPEndPoint"/> of the downstream connection. + /// </summary> + IPEndPoint RemoteEndpoint { get; } + /// <summary> + /// The encoding type used to decode and encode character data to and from the current client + /// </summary> + Encoding Encoding { get; } + /// <summary> + /// A <see cref="IReadOnlyDictionary{TKey, TValue}"/> of client request cookies + /// </summary> + IReadOnlyDictionary<string, string> RequestCookies { get; } + /// <summary> + /// Gets an <see cref="IEnumerator{T}"/> for the parsed accept header values + /// </summary> + IEnumerable<string> Accept { get; } + /// <summary> + /// Gets the underlying transport security information for the current connection + /// </summary> + TransportSecurityInfo? TransportSecurity { get; } + + /// <summary> + /// Determines if the client accepts the response content type + /// </summary> + /// <param name="type">The desired content type</param> + /// <returns>True if the client accepts the content type, false otherwise</returns> + bool Accepts(ContentType type); + + /// <summary> + /// Determines if the client accepts the response content type + /// </summary> + /// <param name="contentType">The desired content type</param> + /// <returns>True if the client accepts the content type, false otherwise</returns> + bool Accepts(string contentType); + + /// <summary> + /// Adds a new cookie to the response. If a cookie with the same name and value + /// has been set, the old cookie is replaced with the new one. + /// </summary> + /// <param name="name">Cookie name/id</param> + /// <param name="value">Value to be stored in cookie</param> + /// <param name="domain">Domain for cookie to operate</param> + /// <param name="path">Path to store cookie</param> + /// <param name="Expires">Timespan representing how long the cookie should exist</param> + /// <param name="sameSite">Samesite attribute, Default = Lax</param> + /// <param name="httpOnly">Specify the HttpOnly flag</param> + /// <param name="secure">Specify the Secure flag</param> + void SetCookie(string name, string value, string? domain, string? path, TimeSpan Expires, CookieSameSite sameSite, bool httpOnly, bool secure); + } +}
\ No newline at end of file diff --git a/Net.Http/src/IHeaderCollection.cs b/Net.Http/src/IHeaderCollection.cs new file mode 100644 index 0000000..f7f147a --- /dev/null +++ b/Net.Http/src/IHeaderCollection.cs @@ -0,0 +1,85 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IHeaderCollection.cs +* +* IHeaderCollection.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Net; +using System.Collections.Generic; + +namespace VNLib.Net.Http +{ + /// <summary> + /// The container for request and response headers + /// </summary> + public interface IHeaderCollection + { + /// <summary> + /// Allows for enumeratring all requesest headers + /// </summary> + IEnumerable<KeyValuePair<string, string>> RequestHeaders { get; } + /// <summary> + /// Allows for enumeratring all response headers + /// </summary> + IEnumerable<KeyValuePair<string, string>> ResponseHeaders { get; } + /// <summary> + /// Gets request header, or sets a response header + /// </summary> + /// <param name="index"></param> + /// <returns>Request header with key</returns> + string? this[string index] { get; set; } + /// <summary> + /// Sets a response header only with a response header index + /// </summary> + /// <param name="index">Response header</param> + string this[HttpResponseHeader index] { set; } + /// <summary> + /// Gets a request header + /// </summary> + /// <param name="index">The request header enum </param> + string? this[HttpRequestHeader index] { get; } + /// <summary> + /// Determines if the given header is set in current response headers + /// </summary> + /// <param name="header">Header value to check response headers for</param> + /// <returns>true if header exists in current response headers, false otherwise</returns> + bool HeaderSet(HttpResponseHeader header); + /// <summary> + /// Determines if the given request header is set in current request headers + /// </summary> + /// <param name="header">Header value to check request headers for</param> + /// <returns>true if header exists in current request headers, false otherwise</returns> + bool HeaderSet(HttpRequestHeader header); + + /// <summary> + /// Overwrites (sets) the given response header to the exact value specified + /// </summary> + /// <param name="header">The enumrated header id</param> + /// <param name="value">The value to specify</param> + void Append(HttpResponseHeader header, string? value); + /// <summary> + /// Overwrites (sets) the given response header to the exact value specified + /// </summary> + /// <param name="header">The header name</param> + /// <param name="value">The value to specify</param> + void Append(string header, string? value); + } +}
\ No newline at end of file diff --git a/Net.Http/src/IMemoryResponseEntity.cs b/Net.Http/src/IMemoryResponseEntity.cs new file mode 100644 index 0000000..aa77f58 --- /dev/null +++ b/Net.Http/src/IMemoryResponseEntity.cs @@ -0,0 +1,69 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IMemoryResponseEntity.cs +* +* IMemoryResponseEntity.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Net.Http +{ + /// <summary> + /// <para> + /// A forward only memory backed response entity body reader. This interface exists + /// to provide a memory-backed response body that will be written "directly" to the + /// response stream. This avoids a buffer allocation and a copy. + /// </para> + /// <para> + /// The entity is only read foward, one time, so it is not seekable. + /// </para> + /// <para> + /// The <see cref="Close"/> method is always called by internal lifecycle hooks + /// when the entity is no longer needed. <see cref="Close"/> should avoid raising + /// excptions. + /// </para> + /// </summary> + public interface IMemoryResponseReader + { + /// <summary> + /// Gets a readonly buffer containing the remaining + /// data to be written + /// </summary> + /// <returns>A memory segment to send to the client</returns> + ReadOnlyMemory<byte> GetMemory(); + + /// <summary> + /// Advances the buffer by the number of bytes written + /// </summary> + /// <param name="written">The number of bytes written</param> + void Advance(int written); + + /// <summary> + /// The number of bytes remaining to send + /// </summary> + int Remaining { get; } + + /// <summary> + /// Raised when reading has completed + /// </summary> + void Close(); + } +}
\ No newline at end of file diff --git a/Net.Http/src/ITransportContext.cs b/Net.Http/src/ITransportContext.cs new file mode 100644 index 0000000..bd6ce05 --- /dev/null +++ b/Net.Http/src/ITransportContext.cs @@ -0,0 +1,71 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: ITransportContext.cs +* +* ITransportContext.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.IO; +using System.Net; +using System.Threading.Tasks; +using System.Security.Authentication; + + +namespace VNLib.Net.Http +{ + /// <summary> + /// Represents an active connection for application data processing + /// </summary> + public interface ITransportContext + { + /// <summary> + /// The transport network stream for application data marshaling + /// </summary> + Stream ConnectionStream { get; } + /// <summary> + /// The transport security layer security protocol + /// </summary> + SslProtocols SslVersion { get; } + /// <summary> + /// A copy of the local endpoint of the listening socket + /// </summary> + IPEndPoint LocalEndPoint { get; } + /// <summary> + /// The <see cref="IPEndPoint"/> representing the client's connection information + /// </summary> + IPEndPoint RemoteEndpoint { get; } + + /// <summary> + /// Closes the connection when its no longer in use and cleans up held resources. + /// </summary> + /// <returns></returns> + /// <remarks> + /// This method will always be called by the server when a connection is complete + /// regardless of the state of the trasnport + /// </remarks> + ValueTask CloseConnectionAsync(); + + /// <summary> + /// Attemts to get the transport security details for the connection + /// </summary> + /// <returns>A the <see cref="TransportSecurityInfo"/> structure if applicable, null otherwise</returns> + TransportSecurityInfo? GetSecurityInfo(); + } +}
\ No newline at end of file diff --git a/Net.Http/src/ITransportProvider.cs b/Net.Http/src/ITransportProvider.cs new file mode 100644 index 0000000..13aae57 --- /dev/null +++ b/Net.Http/src/ITransportProvider.cs @@ -0,0 +1,51 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: ITransportProvider.cs +* +* ITransportProvider.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Net.Http +{ + /// <summary> + /// Listens for network connections and captures the information + /// required for application processing + /// </summary> + public interface ITransportProvider + { + /// <summary> + /// Begins listening for connections (binds a socket if necessary) and is + /// called before the server begins listening for connections. + /// </summary> + /// <param name="stopToken">A token that is cancelled when the server is closed</param> + void Start(CancellationToken stopToken); + + /// <summary> + /// Waits for a new connection to be established and returns its context. This method + /// should only return an established connection (ie: connected socket). + /// </summary> + /// <param name="cancellation">A token to cancel the wait operation</param> + /// <returns>A <see cref="ValueTask"/> that returns an established connection</returns> + ValueTask<ITransportContext> AcceptAsync(CancellationToken cancellation); + } +}
\ No newline at end of file diff --git a/Net.Http/src/IWebRoot.cs b/Net.Http/src/IWebRoot.cs new file mode 100644 index 0000000..9b1a9c8 --- /dev/null +++ b/Net.Http/src/IWebRoot.cs @@ -0,0 +1,58 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: IWebRoot.cs +* +* IWebRoot.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Threading.Tasks; +using System.Collections.Generic; + + +namespace VNLib.Net.Http +{ + /// <summary> + /// Represents a root identifying the main endpoints of the server, and the primary processing actions + /// for requests to this endpoint + /// </summary> + public interface IWebRoot + { + /// <summary> + /// The hostname the server will listen for, and the hostname that will identify this root when a connection requests it + /// </summary> + string Hostname { get; } + /// <summary> + /// <para> + /// The main event handler for user code to process a request + /// </para> + /// <para> + /// NOTE: This function must be thread-safe! + /// </para> + /// </summary> + /// <param name="httpEvent">An active, unprocessed event capturing the request infomration into a standard format</param> + /// <returns>A <see cref="ValueTask"/> that the processor will await until the entity has been processed</returns> + ValueTask ClientConnectedAsync(IHttpEvent httpEvent); + /// <summary> + /// "Low-Level" 301 redirects + /// </summary> + IReadOnlyDictionary<string, Redirect> Redirects { get; } + } +}
\ No newline at end of file diff --git a/Net.Http/src/TransportSecurityInfo.cs b/Net.Http/src/TransportSecurityInfo.cs new file mode 100644 index 0000000..7c7a79c --- /dev/null +++ b/Net.Http/src/TransportSecurityInfo.cs @@ -0,0 +1,121 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Net.Http +* File: TransportSecurityInfo.cs +* +* TransportSecurityInfo.cs is part of VNLib.Net.Http which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Net.Http is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Net.Http is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Net; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + + +namespace VNLib.Net.Http +{ + + /// <summary> + /// Gets the transport TLS security information for the current connection + /// </summary> + public readonly struct TransportSecurityInfo + { + /// <summary> + /// Gets a Boolean value that indicates whether the certificate revocation list is checked during the certificate validation process. + /// </summary> + /// <returns>true if the certificate revocation list is checked during validation; otherwise, false.</returns> + public readonly bool CheckCertRevocationStatus { get; init; } + + /// <summary> + /// Gets a value that identifies the bulk encryption algorithm used by the connection. + /// </summary> + public readonly CipherAlgorithmType CipherAlgorithm { get; init; } + + /// <summary> + /// Gets a value that identifies the strength of the cipher algorithm used by the connection. + /// </summary> + public readonly int CipherStrength { get; init; } + + /// <summary> + /// Gets the algorithm used for generating message authentication codes (MACs). + /// </summary> + public readonly HashAlgorithmType HashAlgorithm { get; init; } + + /// <summary> + /// Gets a value that identifies the strength of the hash algorithm used by this instance. + /// </summary> + public readonly int HashStrength { get; init; } + + /// <summary> + /// Gets a Boolean value that indicates whether authentication was successful. + /// </summary> + public readonly bool IsAuthenticated { get; init; } + + /// <summary> + /// Gets a Boolean value that indicates whether this connection uses data encryption. + /// </summary> + public readonly bool IsEncrypted { get; init; } + + /// <summary> + /// Gets a Boolean value that indicates whether both server and client have been authenticated. + /// </summary> + public readonly bool IsMutuallyAuthenticated { get; init; } + + /// <summary> + /// Gets a Boolean value that indicates whether the data sent using this connection is signed. + /// </summary> + public readonly bool IsSigned { get; init; } + + /// <summary> + /// Gets the key exchange algorithm used by this connection + /// </summary> + public readonly ExchangeAlgorithmType KeyExchangeAlgorithm { get; init; } + + /// <summary> + /// Gets a value that identifies the strength of the key exchange algorithm used by the transport connection + /// </summary> + public readonly int KeyExchangeStrength { get; init; } + + /// <summary> + /// Gets the certificate used to authenticate the local endpoint. + /// </summary> + public readonly X509Certificate? LocalCertificate { get; init; } + + /// <summary> + /// The negotiated application protocol in TLS handshake. + /// </summary> + public readonly SslApplicationProtocol NegotiatedApplicationProtocol { get; init; } + + /// <summary> + /// Gets the cipher suite which was negotiated for this connection. + /// </summary> + public readonly TlsCipherSuite NegotiatedCipherSuite { get; init; } + + /// <summary> + /// Gets the certificate used to authenticate the remote endpoint. + /// </summary> + public readonly X509Certificate? RemoteCertificate { get; init; } + + /// <summary> + /// Gets the TransportContext used for authentication using extended protection. + /// </summary> + public readonly TransportContext TransportContext { get; init; } + } +} diff --git a/Net.Http/src/VNLib.Net.Http.csproj b/Net.Http/src/VNLib.Net.Http.csproj new file mode 100644 index 0000000..30e698c --- /dev/null +++ b/Net.Http/src/VNLib.Net.Http.csproj @@ -0,0 +1,57 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <RootNamespace>VNLib.Net.Http</RootNamespace> + <Authors>Vaughn Nugent</Authors> + <Company>$(Authors)</Company> + <Product>VNLib HTTP Library</Product> + <Description>Provides a high performance HTTP 0.9-1.1 application processing layer for handling transport *agnostic connections and asynchronous event support for applications serving HTTP +requests such as web content. This library has a large focus on low/no GC allocations using unmanaged memory support provided by the VNLib.Utils library. No external dependencies +outside of the VNLib ecosystem are required. The VNLib.Plugins and VNLib.Plugins.Essentials libraries are highly recommended for serving web content.</Description> + <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> + <PackageId>VNLib.Net.Http</PackageId> + <Version>1.0.1.5</Version> + <NeutralLanguage>en-US</NeutralLanguage> + <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> + <AssemblyName>VNLib.Net.Http</AssemblyName> + <Nullable>enable</Nullable> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <AnalysisLevel>latest-all</AnalysisLevel> + <SignAssembly>True</SignAssembly> + <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile> + </PropertyGroup> + + <!-- Resolve nuget dll files and store them in the output dir --> + <PropertyGroup> + <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)'=='Debug'"> + <CheckForOverflowUnderflow>True</CheckForOverflowUnderflow> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> + <Deterministic>False</Deterministic> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> + <Deterministic>False</Deterministic> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\Utils\src\VNLib.Utils.csproj" /> + </ItemGroup> + +</Project> |