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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

@@ -0,0 +1,22 @@
using System;
using System.Net;
using System.Threading.Tasks;
namespace EmbedIO
{
/// <summary>
/// A callback used to provide information about an unhandled exception occurred while processing a request.
/// </summary>
/// <param name="context">An <see cref="IHttpContext" /> interface representing the context of the request.</param>
/// <param name="exception">The unhandled exception.</param>
/// <returns>A <see cref="Task" /> representing the ongoing operation.</returns>
/// <remarks>
/// <para>When this delegate is called, the response's status code has already been set to
/// <see cref="HttpStatusCode.InternalServerError" />.</para>
/// <para>Any exception thrown by a handler (even a HTTP exception) will go unhandled: the web server
/// will not crash, but processing of the request will be aborted, and the response will be flushed as-is.
/// In other words, it is not a good ides to <c>throw HttpException.NotFound()</c> (or similar)
/// from a handler.</para>
/// </remarks>
public delegate Task ExceptionHandlerCallback(IHttpContext context, Exception exception);
}

View File

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

View File

@@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
using EmbedIO.Files.Internal;
namespace EmbedIO.Files
{
public sealed partial class FileCache
{
internal class Section
{
private readonly object _syncRoot = new object();
private readonly Dictionary<string, FileCacheItem> _items = new Dictionary<string, FileCacheItem>(StringComparer.Ordinal);
private long _totalSize;
private string? _oldestKey;
private string? _newestKey;
public void Clear()
{
lock (_syncRoot)
{
ClearCore();
}
}
public void Add(string path, FileCacheItem item)
{
lock (_syncRoot)
{
AddItemCore(path, item);
}
}
public void Remove(string path)
{
lock (_syncRoot)
{
RemoveItemCore(path);
}
}
public bool TryGet(string path, out FileCacheItem item)
{
lock (_syncRoot)
{
if (!_items.TryGetValue(path, out item))
return false;
RefreshItemCore(path, item);
return true;
}
}
internal long GetLeastRecentUseTime()
{
lock (_syncRoot)
{
return _oldestKey == null ? long.MaxValue : _items[_oldestKey].LastUsedAt;
}
}
// Removes least recently used item.
// Returns size of removed item.
internal long RemoveLeastRecentItem()
{
lock (_syncRoot)
{
return RemoveLeastRecentItemCore();
}
}
internal long GetTotalSize()
{
lock (_syncRoot)
{
return _totalSize;
}
}
internal void UpdateTotalSize(long delta)
{
lock (_syncRoot)
{
_totalSize += delta;
}
}
private void ClearCore()
{
_items.Clear();
_totalSize = 0;
_oldestKey = null;
_newestKey = null;
}
// Adds an item as most recently used.
private void AddItemCore(string path, FileCacheItem item)
{
item.PreviousKey = _newestKey;
item.NextKey = null;
item.LastUsedAt = TimeBase.ElapsedTicks;
if (_newestKey != null)
_items[_newestKey].NextKey = path;
_newestKey = path;
_items[path] = item;
_totalSize += item.SizeInCache;
}
// Removes an item.
private void RemoveItemCore(string path)
{
if (!_items.TryGetValue(path, out var item))
return;
if (_oldestKey == path)
_oldestKey = item.NextKey;
if (_newestKey == path)
_newestKey = item.PreviousKey;
if (item.PreviousKey != null)
_items[item.PreviousKey].NextKey = item.NextKey;
if (item.NextKey != null)
_items[item.NextKey].PreviousKey = item.PreviousKey;
item.PreviousKey = null;
item.NextKey = null;
_items.Remove(path);
_totalSize -= item.SizeInCache;
}
// Removes the least recently used item.
// returns size of removed item.
private long RemoveLeastRecentItemCore()
{
var path = _oldestKey;
if (path == null)
return 0;
var item = _items[path];
if ((_oldestKey = item.NextKey) != null)
_items[_oldestKey].PreviousKey = null;
if (_newestKey == path)
_newestKey = null;
item.PreviousKey = null;
item.NextKey = null;
_items.Remove(path);
_totalSize -= item.SizeInCache;
return item.SizeInCache;
}
// Moves an item to most recently used.
private void RefreshItemCore(string path, FileCacheItem item)
{
item.LastUsedAt = TimeBase.ElapsedTicks;
if (_newestKey == path)
return;
if (_oldestKey == path)
_oldestKey = item.NextKey;
if (item.PreviousKey != null)
_items[item.PreviousKey].NextKey = item.NextKey;
if (item.NextKey != null)
_items[item.NextKey].PreviousKey = item.PreviousKey;
item.PreviousKey = _newestKey;
item.NextKey = null;
_items[_newestKey!].NextKey = path;
_newestKey = path;
}
}
}
}

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

@@ -0,0 +1,178 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EmbedIO.Internal;
using Swan.Threading;
using Swan.Logging;
namespace EmbedIO.Files
{
#pragma warning disable CA1001 // Type owns disposable field '_cleaner' but is not disposable - _cleaner has its own dispose semantics.
/// <summary>
/// A cache where one or more instances of <see cref="FileModule"/> can store hashes and file contents.
/// </summary>
public sealed partial class FileCache
#pragma warning restore CA1001
{
/// <summary>
/// The default value for the <see cref="MaxSizeKb"/> property.
/// </summary>
public const int DefaultMaxSizeKb = 10240;
/// <summary>
/// The default value for the <see cref="MaxFileSizeKb"/> property.
/// </summary>
public const int DefaultMaxFileSizeKb = 200;
private static readonly Stopwatch TimeBase = Stopwatch.StartNew();
private static readonly object DefaultSyncRoot = new object();
private static FileCache? _defaultInstance;
private readonly ConcurrentDictionary<string, Section> _sections = new ConcurrentDictionary<string, Section>(StringComparer.Ordinal);
private int _sectionCount; // Because ConcurrentDictionary<,>.Count is locking.
private int _maxSizeKb = DefaultMaxSizeKb;
private int _maxFileSizeKb = DefaultMaxFileSizeKb;
private PeriodicTask? _cleaner;
/// <summary>
/// Gets the default <see cref="FileCache"/> instance used by <see cref="FileModule"/>.
/// </summary>
public static FileCache Default
{
get
{
if (_defaultInstance != null)
return _defaultInstance;
lock (DefaultSyncRoot)
{
if (_defaultInstance == null)
_defaultInstance = new FileCache();
}
return _defaultInstance;
}
}
/// <summary>
/// <para>Gets or sets the maximum total size of cached data in kilobytes (1 kilobyte = 1024 bytes).</para>
/// <para>The default value for this property is stored in the <see cref="DefaultMaxSizeKb"/> constant field.</para>
/// <para>Setting this property to a value less lower han 1 has the same effect as setting it to 1.</para>
/// </summary>
public int MaxSizeKb
{
get => _maxSizeKb;
set => _maxSizeKb = Math.Max(value, 1);
}
/// <summary>
/// <para>Gets or sets the maximum size of a single cached file in kilobytes (1 kilobyte = 1024 bytes).</para>
/// <para>A single file's contents may be present in a cache more than once, if the file
/// is requested with different <c>Accept-Encoding</c> request headers. This property acts as a threshold
/// for the uncompressed size of a file.</para>
/// <para>The default value for this property is stored in the <see cref="DefaultMaxFileSizeKb"/> constant field.</para>
/// <para>Setting this property to a value lower than 0 has the same effect as setting it to 0, in fact
/// completely disabling the caching of file contents for this cache.</para>
/// <para>This property cannot be set to a value higher than 2097151; in other words, it is not possible
/// to cache files bigger than two Gigabytes (1 Gigabyte = 1048576 kilobytes) minus 1 kilobyte.</para>
/// </summary>
public int MaxFileSizeKb
{
get => _maxFileSizeKb;
set => _maxFileSizeKb = Math.Min(Math.Max(value, 0), 2097151);
}
// Cast as IDictionary because we WANT an exception to be thrown if the name exists.
// It would mean that something is very, very wrong.
internal Section AddSection(string name)
{
var section = new Section();
(_sections as IDictionary<string, Section>).Add(name, section);
if (Interlocked.Increment(ref _sectionCount) == 1)
_cleaner = new PeriodicTask(TimeSpan.FromMinutes(1), CheckMaxSize);
return section;
}
internal void RemoveSection(string name)
{
_sections.TryRemove(name, out _);
if (Interlocked.Decrement(ref _sectionCount) == 0)
{
_cleaner?.Dispose();
_cleaner = null;
}
}
private async Task CheckMaxSize(CancellationToken cancellationToken)
{
var timeKeeper = new TimeKeeper();
var maxSizeKb = _maxSizeKb;
var initialSizeKb = ComputeTotalSize() / 1024L;
if (initialSizeKb <= maxSizeKb)
{
$"Total size = {initialSizeKb}/{_maxSizeKb}kb, not purging.".Debug(nameof(FileCache));
return;
}
$"Total size = {initialSizeKb}/{_maxSizeKb}kb, purging...".Debug(nameof(FileCache));
var removedCount = 0;
var removedSize = 0L;
var totalSizeKb = initialSizeKb;
var threshold = 973L * maxSizeKb / 1024L; // About 95% of maximum allowed size
while (totalSizeKb > threshold)
{
if (cancellationToken.IsCancellationRequested)
return;
var section = GetSectionWithLeastRecentItem();
if (section == null)
return;
removedSize += section.RemoveLeastRecentItem();
removedCount++;
await Task.Yield();
totalSizeKb = ComputeTotalSize() / 1024L;
}
$"Purge completed in {timeKeeper.ElapsedTime}ms: removed {removedCount} items ({removedSize / 1024L}kb). Total size is now {totalSizeKb}kb."
.Debug(nameof(FileCache));
}
// Enumerate key / value pairs because the Keys and Values property
// of ConcurrentDictionary<,> have snapshot semantics,
// while GetEnumerator enumerates without locking.
private long ComputeTotalSize()
=> _sections.Sum(pair => pair.Value.GetTotalSize());
private Section? GetSectionWithLeastRecentItem()
{
Section? result = null;
var earliestTime = long.MaxValue;
foreach (var pair in _sections)
{
var section = pair.Value;
var time = section.GetLeastRecentUseTime();
if (time < earliestTime)
{
result = section;
earliestTime = time;
}
}
return result;
}
}
}

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

@@ -0,0 +1,635 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using EmbedIO.Files.Internal;
using EmbedIO.Internal;
using EmbedIO.Utilities;
namespace EmbedIO.Files
{
/// <summary>
/// A module serving files and directory listings from an <see cref="IFileProvider"/>.
/// </summary>
/// <seealso cref="WebModuleBase" />
public class FileModule : WebModuleBase, IDisposable, IMimeTypeCustomizer
{
/// <summary>
/// <para>Default value for <see cref="DefaultDocument"/>.</para>
/// </summary>
public const string DefaultDocumentName = "index.html";
private readonly string _cacheSectionName = UniqueIdGenerator.GetNext();
private readonly MimeTypeCustomizer _mimeTypeCustomizer = new MimeTypeCustomizer();
private readonly ConcurrentDictionary<string, MappedResourceInfo>? _mappingCache;
private FileCache _cache = FileCache.Default;
private bool _contentCaching = true;
private string? _defaultDocument = DefaultDocumentName;
private string? _defaultExtension;
private IDirectoryLister? _directoryLister;
private FileRequestHandlerCallback _onMappingFailed = FileRequestHandler.ThrowNotFound;
private FileRequestHandlerCallback _onDirectoryNotListable = FileRequestHandler.ThrowUnauthorized;
private FileRequestHandlerCallback _onMethodNotAllowed = FileRequestHandler.ThrowMethodNotAllowed;
private FileCache.Section? _cacheSection;
/// <summary>
/// Initializes a new instance of the <see cref="FileModule"/> class,
/// using the specified cache.
/// </summary>
/// <param name="baseRoute">The base route.</param>
/// <param name="provider">An <see cref="IFileProvider"/> interface that provides access
/// to actual files and directories.</param>
/// <exception cref="ArgumentNullException"><paramref name="provider"/> is <see langword="null"/>.</exception>
public FileModule(string baseRoute, IFileProvider provider)
: base(baseRoute)
{
Provider = Validate.NotNull(nameof(provider), provider);
_mappingCache = Provider.IsImmutable
? new ConcurrentDictionary<string, MappedResourceInfo>()
: null;
}
/// <summary>
/// Finalizes an instance of the <see cref="FileModule"/> class.
/// </summary>
~FileModule()
{
Dispose(false);
}
/// <inheritdoc />
public override bool IsFinalHandler => true;
/// <summary>
/// Gets the <see cref="IFileProvider"/>interface that provides access
/// to actual files and directories served by this module.
/// </summary>
public IFileProvider Provider { get; }
/// <summary>
/// Gets or sets the <see cref="FileCache"/> used by this module to store hashes and,
/// optionally, file contents and rendered directory listings.
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
/// <exception cref="ArgumentNullException">This property is being set to <see langword="null"/>.</exception>
public FileCache Cache
{
get => _cache;
set
{
EnsureConfigurationNotLocked();
_cache = Validate.NotNull(nameof(value), value);
}
}
/// <summary>
/// <para>Gets or sets a value indicating whether this module caches the contents of files
/// and directory listings.</para>
/// <para>Note that the actual representations of files are stored in <see cref="FileCache"/>;
/// thus, for example, if a file is always requested with an <c>Accept-Encoding</c> of <c>gzip</c>,
/// only the gzipped contents of the file will be cached.</para>
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
public bool ContentCaching
{
get => _contentCaching;
set
{
EnsureConfigurationNotLocked();
_contentCaching = value;
}
}
/// <summary>
/// <para>Gets or sets the name of the default document served, if it exists, instead of a directory listing
/// when the path of a requested URL maps to a directory.</para>
/// <para>The default value for this property is the <see cref="DefaultDocumentName"/> constant.</para>
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
public string? DefaultDocument
{
get => _defaultDocument;
set
{
EnsureConfigurationNotLocked();
_defaultDocument = string.IsNullOrEmpty(value) ? null : value;
}
}
/// <summary>
/// <para>Gets or sets the default extension appended to requested URL paths that do not map
/// to any file or directory. Defaults to <see langword="null"/>.</para>
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
/// <exception cref="ArgumentException">This property is being set to a non-<see langword="null"/>,
/// non-empty string that does not start with a period (<c>.</c>).</exception>
public string? DefaultExtension
{
get => _defaultExtension;
set
{
EnsureConfigurationNotLocked();
if (string.IsNullOrEmpty(value))
{
_defaultExtension = null;
}
else if (value![0] != '.')
{
throw new ArgumentException("Default extension does not start with a period.", nameof(value));
}
else
{
_defaultExtension = value;
}
}
}
/// <summary>
/// <para>Gets or sets the <see cref="IDirectoryLister"/> interface used to generate
/// directory listing in this module.</para>
/// <para>A value of <see langword="null"/> (the default) disables the generation
/// of directory listings.</para>
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
public IDirectoryLister? DirectoryLister
{
get => _directoryLister;
set
{
EnsureConfigurationNotLocked();
_directoryLister = value;
}
}
/// <summary>
/// <para>Gets or sets a <see cref="FileRequestHandlerCallback"/> that is called whenever
/// the requested URL path could not be mapped to any file or directory.</para>
/// <para>The default is <see cref="FileRequestHandler.ThrowNotFound"/>.</para>
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
/// <exception cref="ArgumentNullException">This property is being set to <see langword="null"/>.</exception>
/// <seealso cref="FileRequestHandler"/>
public FileRequestHandlerCallback OnMappingFailed
{
get => _onMappingFailed;
set
{
EnsureConfigurationNotLocked();
_onMappingFailed = Validate.NotNull(nameof(value), value);
}
}
/// <summary>
/// <para>Gets or sets a <see cref="FileRequestHandlerCallback"/> that is called whenever
/// the requested URL path has been mapped to a directory, but directory listing has been
/// disabled by setting <see cref="DirectoryLister"/> to <see langword="null"/>.</para>
/// <para>The default is <see cref="FileRequestHandler.ThrowUnauthorized"/>.</para>
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
/// <exception cref="ArgumentNullException">This property is being set to <see langword="null"/>.</exception>
/// <seealso cref="FileRequestHandler"/>
public FileRequestHandlerCallback OnDirectoryNotListable
{
get => _onDirectoryNotListable;
set
{
EnsureConfigurationNotLocked();
_onDirectoryNotListable = Validate.NotNull(nameof(value), value);
}
}
/// <summary>
/// <para>Gets or sets a <see cref="FileRequestHandlerCallback"/> that is called whenever
/// the requested URL path has been mapped to a file or directory, but the request's
/// HTTP method is neither <c>GET</c> nor <c>HEAD</c>.</para>
/// <para>The default is <see cref="FileRequestHandler.ThrowMethodNotAllowed"/>.</para>
/// </summary>
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
/// <exception cref="ArgumentNullException">This property is being set to <see langword="null"/>.</exception>
/// <seealso cref="FileRequestHandler"/>
public FileRequestHandlerCallback OnMethodNotAllowed
{
get => _onMethodNotAllowed;
set
{
EnsureConfigurationNotLocked();
_onMethodNotAllowed = Validate.NotNull(nameof(value), value);
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
string IMimeTypeProvider.GetMimeType(string extension)
=> _mimeTypeCustomizer.GetMimeType(extension);
bool IMimeTypeProvider.TryDetermineCompression(string mimeType, out bool preferCompression)
=> _mimeTypeCustomizer.TryDetermineCompression(mimeType, out preferCompression);
/// <inheritdoc />
public void AddCustomMimeType(string extension, string mimeType)
=> _mimeTypeCustomizer.AddCustomMimeType(extension, mimeType);
/// <inheritdoc />
public void PreferCompression(string mimeType, bool preferCompression)
=> _mimeTypeCustomizer.PreferCompression(mimeType, preferCompression);
/// <summary>
/// Clears the part of <see cref="Cache"/> used by this module.
/// </summary>
public void ClearCache()
{
_mappingCache?.Clear();
_cacheSection?.Clear();
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources;
/// <see langword="false"/> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!disposing)
return;
if (_cacheSection != null)
Provider.ResourceChanged -= _cacheSection.Remove;
if (Provider is IDisposable disposableProvider)
disposableProvider.Dispose();
if (_cacheSection != null)
Cache.RemoveSection(_cacheSectionName);
}
/// <inheritdoc />
protected override void OnBeforeLockConfiguration()
{
base.OnBeforeLockConfiguration();
_mimeTypeCustomizer.Lock();
}
/// <inheritdoc />
protected override void OnStart(CancellationToken cancellationToken)
{
base.OnStart(cancellationToken);
_cacheSection = Cache.AddSection(_cacheSectionName);
Provider.ResourceChanged += _cacheSection.Remove;
Provider.Start(cancellationToken);
}
/// <inheritdoc />
protected override async Task OnRequestAsync(IHttpContext context)
{
MappedResourceInfo? info;
var path = context.RequestedPath;
// Map the URL path to a mapped resource.
// DefaultDocument and DefaultExtension are handled here.
// Use the mapping cache if it exists.
if (_mappingCache == null)
{
info = MapUrlPath(path, context);
}
else if (!_mappingCache.TryGetValue(path, out info))
{
info = MapUrlPath(path, context);
if (info != null)
_ = _mappingCache.AddOrUpdate(path, info, (_, __) => info);
}
if (info == null)
{
// If mapping failed, send a "404 Not Found" response, or whatever OnMappingFailed chooses to do.
// For example, it may return a default resource (think a folder of images and an imageNotFound.jpg),
// or redirect the request.
await OnMappingFailed(context, null).ConfigureAwait(false);
}
else if (!IsHttpMethodAllowed(context.Request, out var sendResponseBody))
{
// If there is a mapped resource, check that the HTTP method is either GET or HEAD.
// Otherwise, send a "405 Method Not Allowed" response, or whatever OnMethodNotAllowed chooses to do.
await OnMethodNotAllowed(context, info).ConfigureAwait(false);
}
else if (info.IsDirectory && DirectoryLister == null)
{
// If a directory listing was requested, but there is no DirectoryLister,
// send a "403 Unauthorized" response, or whatever OnDirectoryNotListable chooses to do.
// For example, one could prefer to send "404 Not Found" instead.
await OnDirectoryNotListable(context, info).ConfigureAwait(false);
}
else
{
await HandleResource(context, info, sendResponseBody).ConfigureAwait(false);
}
}
// Tells whether a request's HTTP method is suitable for processing by FileModule
// and, if so, whether a response body must be sent.
private static bool IsHttpMethodAllowed(IHttpRequest request, out bool sendResponseBody)
{
switch (request.HttpVerb)
{
case HttpVerbs.Head:
sendResponseBody = false;
return true;
case HttpVerbs.Get:
sendResponseBody = true;
return true;
default:
sendResponseBody = default;
return false;
}
}
// Prepares response headers for a "200 OK" or "304 Not Modified" response.
// RFC7232, Section 4.1
private static void PreparePositiveResponse(IHttpResponse response, MappedResourceInfo info, string contentType, string entityTag, Action<IHttpResponse> setCompression)
{
setCompression(response);
response.ContentType = contentType;
response.Headers.Set(HttpHeaderNames.ETag, entityTag);
response.Headers.Set(HttpHeaderNames.LastModified, HttpDate.Format(info.LastModifiedUtc));
response.Headers.Set(HttpHeaderNames.CacheControl, "max-age=0, must-revalidate");
response.Headers.Set(HttpHeaderNames.AcceptRanges, "bytes");
}
// Attempts to map a module-relative URL path to a mapped resource,
// handling DefaultDocument and DefaultExtension.
// Returns null if not found.
// Directories mus be returned regardless of directory listing being enabled.
private MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider)
{
var result = Provider.MapUrlPath(urlPath, mimeTypeProvider);
// If urlPath maps to a file, no further searching is needed.
if (result?.IsFile ?? false)
return result;
// Look for a default document.
// Don't append an additional slash if the URL path is "/".
// The default document, if found, must be a file, not a directory.
if (DefaultDocument != null)
{
var defaultDocumentPath = urlPath + (urlPath.Length > 1 ? "/" : string.Empty) + DefaultDocument;
var defaultDocumentResult = Provider.MapUrlPath(defaultDocumentPath, mimeTypeProvider);
if (defaultDocumentResult?.IsFile ?? false)
return defaultDocumentResult;
}
// Try to apply default extension (but not if the URL path is "/",
// i.e. the only normalized, non-base URL path that ends in a slash).
// When the default extension is applied, the result must be a file.
if (DefaultExtension != null && urlPath.Length > 1)
{
var defaultExtensionResult = Provider.MapUrlPath(urlPath + DefaultExtension, mimeTypeProvider);
if (defaultExtensionResult?.IsFile ?? false)
return defaultExtensionResult;
}
return result;
}
private async Task HandleResource(IHttpContext context, MappedResourceInfo info, bool sendResponseBody)
{
// Try to extract resource information from cache.
var cachingThreshold = 1024L * Cache.MaxFileSizeKb;
if (!_cacheSection!.TryGet(info.Path, out var cacheItem))
{
// Resource information not yet cached
cacheItem = new FileCacheItem(_cacheSection, info.LastModifiedUtc, info.Length);
_cacheSection.Add(info.Path, cacheItem);
}
else if (!Provider.IsImmutable)
{
// Check whether the resource has changed.
// If so, discard the cache item and create a new one.
if (cacheItem.LastModifiedUtc != info.LastModifiedUtc || cacheItem.Length != info.Length)
{
_cacheSection.Remove(info.Path);
cacheItem = new FileCacheItem(_cacheSection, info.LastModifiedUtc, info.Length);
_cacheSection.Add(info.Path, cacheItem);
}
}
/*
* Now we have a cacheItem for the resource.
* It may have been just created, or it may or may not have a cached content,
* depending upon the value of the ContentCaching property,
* the size of the resource, and the value of the
* MaxFileSizeKb of our Cache.
*/
// If the content type is not a valid MIME type, assume the default.
var contentType = info.ContentType ?? DirectoryLister?.ContentType ?? MimeType.Default;
var mimeType = MimeType.StripParameters(contentType);
if (!MimeType.IsMimeType(mimeType, false))
contentType = mimeType = MimeType.Default;
// Next we're going to apply proactive negotiation
// to determine whether we agree with the client upon the compression
// (or lack of it) to use for the resource.
//
// The combination of partial responses and entity compression
// is not really standardized and could lead to a world of pain.
// Thus, if there is a Range header in the request, try to negotiate for no compression.
// Later, if there is compression anyway, we will ignore the Range header.
if (!context.TryDetermineCompression(mimeType, out var preferCompression))
preferCompression = true;
preferCompression &= context.Request.Headers.Get(HttpHeaderNames.Range) == null;
if (!context.Request.TryNegotiateContentEncoding(preferCompression, out var compressionMethod, out var setCompressionInResponse))
{
// If negotiation failed, the returned callback will do the right thing.
setCompressionInResponse(context.Response);
return;
}
var entityTag = info.GetEntityTag(compressionMethod);
// Send a "304 Not Modified" response if applicable.
//
// RFC7232, Section 3.3: "A recipient MUST ignore If-Modified-Since
// if the request contains an If-None-Match header field."
if (context.Request.CheckIfNoneMatch(entityTag, out var ifNoneMatchExists)
|| (!ifNoneMatchExists && context.Request.CheckIfModifiedSince(info.LastModifiedUtc, out _)))
{
context.Response.StatusCode = (int)HttpStatusCode.NotModified;
PreparePositiveResponse(context.Response, info, contentType, entityTag, setCompressionInResponse);
return;
}
/*
* At this point we know the response is "200 OK",
* unless the request is a range request.
*
* RFC7233, Section 3.1: "The Range header field is evaluated after evaluating the precondition
* header fields defined in RFC7232, and only if the result in absence
* of the Range header field would be a 200 (OK) response. In other
* words, Range is ignored when a conditional GET would result in a 304
* (Not Modified) response."
*/
// Before evaluating ranges, we must know the content length.
// This is easy for files, as it is stored in info.Length.
// Directories always have info.Length == 0; therefore,
// unless the directory listing is cached, we must generate it now
// (and cache it while we're there, if applicable).
var content = cacheItem.GetContent(compressionMethod);
if (info.IsDirectory && content == null)
{
long uncompressedLength;
(content, uncompressedLength) = await GenerateDirectoryListingAsync(context, info, compressionMethod)
.ConfigureAwait(false);
if (ContentCaching && uncompressedLength <= cachingThreshold)
_ = cacheItem.SetContent(compressionMethod, content);
}
var contentLength = content?.Length ?? info.Length;
// Ignore range request is compression is enabled
// (or should I say forced, since negotiation has tried not to use it).
var partialStart = 0L;
var partialUpperBound = contentLength - 1;
var isPartial = compressionMethod == CompressionMethod.None
&& context.Request.IsRangeRequest(contentLength, entityTag, info.LastModifiedUtc, out partialStart, out partialUpperBound);
var responseContentLength = contentLength;
if (isPartial)
{
// Prepare a "206 Partial Content" response.
responseContentLength = partialUpperBound - partialStart + 1;
context.Response.StatusCode = (int)HttpStatusCode.PartialContent;
PreparePositiveResponse(context.Response, info, contentType, entityTag, setCompressionInResponse);
context.Response.Headers.Set(HttpHeaderNames.ContentRange, $"bytes {partialStart}-{partialUpperBound}/{contentLength}");
}
else
{
// Prepare a "200 OK" response.
PreparePositiveResponse(context.Response, info, contentType, entityTag, setCompressionInResponse);
}
// If it's a HEAD request, we're done.
if (!sendResponseBody)
return;
// If content must be sent AND cached, first read it and store it.
// If the requested resource is a directory, we have already listed it by now,
// so it must be a file for content to be null.
if (content == null && ContentCaching && contentLength <= cachingThreshold)
{
using (var memoryStream = new MemoryStream())
{
using (var compressor = new CompressionStream(memoryStream, compressionMethod))
{
using var source = Provider.OpenFile(info.Path);
await source.CopyToAsync(compressor, WebServer.StreamCopyBufferSize, context.CancellationToken)
.ConfigureAwait(false);
}
content = memoryStream.ToArray();
responseContentLength = content.Length;
}
_ = cacheItem.SetContent(compressionMethod, content);
}
// Transfer cached content if present.
if (content != null)
{
context.Response.ContentLength64 = responseContentLength;
var offset = isPartial ? (int) partialStart : 0;
await context.Response.OutputStream.WriteAsync(content, offset, (int)responseContentLength, context.CancellationToken)
.ConfigureAwait(false);
return;
}
// Read and transfer content without caching.
using (var source = Provider.OpenFile(info.Path))
{
context.Response.SendChunked = true;
if (isPartial)
{
var buffer = new byte[WebServer.StreamCopyBufferSize];
if (source.CanSeek)
{
source.Position = partialStart;
}
else
{
var skipLength = (int)partialStart;
while (skipLength > 0)
{
var read = await source.ReadAsync(buffer, 0, Math.Min(skipLength, buffer.Length), context.CancellationToken)
.ConfigureAwait(false);
skipLength -= read;
}
}
var transferSize = responseContentLength;
while (transferSize >= WebServer.StreamCopyBufferSize)
{
var read = await source.ReadAsync(buffer, 0, WebServer.StreamCopyBufferSize, context.CancellationToken)
.ConfigureAwait(false);
await context.Response.OutputStream.WriteAsync(buffer, 0, read, context.CancellationToken)
.ConfigureAwait(false);
transferSize -= read;
}
if (transferSize > 0)
{
var read = await source.ReadAsync(buffer, 0, (int)transferSize, context.CancellationToken)
.ConfigureAwait(false);
await context.Response.OutputStream.WriteAsync(buffer, 0, read, context.CancellationToken)
.ConfigureAwait(false);
}
}
else
{
using var compressor = new CompressionStream(context.Response.OutputStream, compressionMethod);
await source.CopyToAsync(compressor, WebServer.StreamCopyBufferSize, context.CancellationToken)
.ConfigureAwait(false);
}
}
}
// Uses DirectoryLister to generate a directory listing asynchronously.
// Returns a tuple of the generated content and its *uncompressed* length
// (useful to decide whether it can be cached).
private async Task<(byte[], long)> GenerateDirectoryListingAsync(
IHttpContext context,
MappedResourceInfo info,
CompressionMethod compressionMethod)
{
using var memoryStream = new MemoryStream();
using var stream = new CompressionStream(memoryStream, compressionMethod);
await DirectoryLister!.ListDirectoryAsync(
info,
context.Request.Url.AbsolutePath,
Provider.GetDirectoryEntries(info.Path, context),
stream,
context.CancellationToken).ConfigureAwait(false);
return (memoryStream.ToArray(), stream.UncompressedLength);
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using EmbedIO.Utilities;
namespace EmbedIO.Files
{
/// <summary>
/// Provides access to the local file system to a <see cref="FileModule"/>.
/// </summary>
/// <seealso cref="IFileProvider" />
public class FileSystemProvider : IDisposable, IFileProvider
{
private readonly FileSystemWatcher? _watcher;
/// <summary>
/// Initializes a new instance of the <see cref="FileSystemProvider"/> class.
/// </summary>
/// <remarks>
/// OSX doesn't support <see cref="FileSystemWatcher" />, the parameter <paramref name="isImmutable" /> will be always <see langword="true"/>.
/// </remarks>
/// <param name="fileSystemPath">The file system path.</param>
/// <param name="isImmutable"><see langword="true"/> if files and directories in
/// <paramref name="fileSystemPath"/> are not expected to change during a web server's
/// lifetime; <see langword="false"/> otherwise.</param>
/// <exception cref="ArgumentNullException"><paramref name="fileSystemPath"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="fileSystemPath"/> is not a valid local path.</exception>
/// <seealso cref="Validate.LocalPath"/>
public FileSystemProvider(string fileSystemPath, bool isImmutable)
{
FileSystemPath = Validate.LocalPath(nameof(fileSystemPath), fileSystemPath, true);
IsImmutable = isImmutable || RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
try
{
if (!IsImmutable)
_watcher = new FileSystemWatcher(FileSystemPath);
}
catch (PlatformNotSupportedException)
{
IsImmutable = true;
}
}
/// <summary>
/// Finalizes an instance of the <see cref="FileSystemProvider"/> class.
/// </summary>
~FileSystemProvider()
{
Dispose(false);
}
/// <inheritdoc />
public event Action<string>? ResourceChanged;
/// <summary>
/// Gets the file system path from which files are retrieved.
/// </summary>
public string FileSystemPath { get; }
/// <inheritdoc />
public bool IsImmutable { get; }
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public void Start(CancellationToken cancellationToken)
{
if (_watcher != null)
{
_watcher.Changed += Watcher_ChangedOrDeleted;
_watcher.Deleted += Watcher_ChangedOrDeleted;
_watcher.Renamed += Watcher_Renamed;
_watcher.EnableRaisingEvents = true;
}
}
/// <inheritdoc />
public MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider)
{
urlPath = urlPath.Substring(1); // Drop the initial slash
string localPath;
// Disable CA1031 as there's little we can do if IsPathRooted or GetFullPath fails.
#pragma warning disable CA1031
try
{
// Unescape the url before continue
urlPath = Uri.UnescapeDataString(urlPath);
// Bail out early if the path is a rooted path,
// as Path.Combine would ignore our base path.
// See https://docs.microsoft.com/en-us/dotnet/api/system.io.path.combine
// (particularly the Remarks section).
//
// Under Windows, a relative URL path may be a full filesystem path
// (e.g. "D:\foo\bar" or "\\192.168.0.1\Shared\MyDocuments\BankAccounts.docx").
// Under Unix-like operating systems we have no such problems, as relativeUrlPath
// can never start with a slash; however, loading one more class from Swan
// just to check the OS type would probably outweigh calling IsPathRooted.
if (Path.IsPathRooted(urlPath))
return null;
// Convert the relative URL path to a relative filesystem path
// (practically a no-op under Unix-like operating systems)
// and combine it with our base local path to obtain a full path.
localPath = Path.Combine(FileSystemPath, urlPath.Replace('/', Path.DirectorySeparatorChar));
// Use GetFullPath as an additional safety check
// for relative paths that contain a rooted path
// (e.g. "valid/path/C:\Windows\System.ini")
localPath = Path.GetFullPath(localPath);
}
catch
{
// Both IsPathRooted and GetFullPath throw exceptions
// if a path contains invalid characters or is otherwise invalid;
// bail out in this case too, as the path would not exist on disk anyway.
return null;
}
#pragma warning restore CA1031
// As a final precaution, check that the resulting local path
// is inside the folder intended to be served.
if (!localPath.StartsWith(FileSystemPath, StringComparison.Ordinal))
return null;
if (File.Exists(localPath))
return GetMappedFileInfo(mimeTypeProvider, localPath);
if (Directory.Exists(localPath))
return GetMappedDirectoryInfo(localPath);
return null;
}
/// <inheritdoc />
public Stream OpenFile(string path) => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
/// <inheritdoc />
public IEnumerable<MappedResourceInfo> GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider)
=> new DirectoryInfo(path).EnumerateFileSystemInfos()
.Select(fsi => GetMappedResourceInfo(mimeTypeProvider, fsi));
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources;
/// <see langword="false"/> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
ResourceChanged = null; // Release references to listeners
if (_watcher != null)
{
_watcher.EnableRaisingEvents = false;
_watcher.Changed -= Watcher_ChangedOrDeleted;
_watcher.Deleted -= Watcher_ChangedOrDeleted;
_watcher.Renamed -= Watcher_Renamed;
if (disposing)
_watcher.Dispose();
}
}
private static MappedResourceInfo GetMappedFileInfo(IMimeTypeProvider mimeTypeProvider, string localPath)
=> GetMappedFileInfo(mimeTypeProvider, new FileInfo(localPath));
private static MappedResourceInfo GetMappedFileInfo(IMimeTypeProvider mimeTypeProvider, FileInfo info)
=> MappedResourceInfo.ForFile(
info.FullName,
info.Name,
info.LastWriteTimeUtc,
info.Length,
mimeTypeProvider.GetMimeType(info.Extension));
private static MappedResourceInfo GetMappedDirectoryInfo(string localPath)
=> GetMappedDirectoryInfo(new DirectoryInfo(localPath));
private static MappedResourceInfo GetMappedDirectoryInfo(DirectoryInfo info)
=> MappedResourceInfo.ForDirectory(info.FullName, info.Name, info.LastWriteTimeUtc);
private static MappedResourceInfo GetMappedResourceInfo(IMimeTypeProvider mimeTypeProvider, FileSystemInfo info)
=> info is DirectoryInfo directoryInfo
? GetMappedDirectoryInfo(directoryInfo)
: GetMappedFileInfo(mimeTypeProvider, (FileInfo) info);
private void Watcher_ChangedOrDeleted(object sender, FileSystemEventArgs e)
=> ResourceChanged?.Invoke(e.FullPath);
private void Watcher_Renamed(object sender, RenamedEventArgs e)
=> ResourceChanged?.Invoke(e.OldFullPath);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
using System;
namespace EmbedIO.Files.Internal
{
internal static class Base64Utility
{
// long is 8 bytes
// base64 of 8 bytes is 12 chars, but the last one is padding
public static string LongToBase64(long value)
=> Convert.ToBase64String(BitConverter.GetBytes(value)).Substring(0, 11);
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Text;
namespace EmbedIO.Files.Internal
{
internal static class EntityTag
{
public static string Compute(DateTime lastModifiedUtc, long length, CompressionMethod compressionMethod)
{
var sb = new StringBuilder()
.Append('"')
.Append(Base64Utility.LongToBase64(lastModifiedUtc.Ticks))
.Append(Base64Utility.LongToBase64(length));
switch (compressionMethod)
{
case CompressionMethod.Deflate:
sb.Append('-').Append(CompressionMethodNames.Deflate);
break;
case CompressionMethod.Gzip:
sb.Append('-').Append(CompressionMethodNames.Gzip);
break;
}
return sb.Append('"').ToString();
}
}
}

View File

@@ -0,0 +1,164 @@
using System;
using EmbedIO.Internal;
namespace EmbedIO.Files.Internal
{
internal sealed class FileCacheItem
{
#pragma warning disable SA1401 // Field should be private - performance is a stronger concern here.
// These fields create a sort of linked list of items
// inside the cache's dictionary.
// Their purpose is to keep track of items
// in order from least to most recently used.
internal string? PreviousKey;
internal string? NextKey;
internal long LastUsedAt;
#pragma warning restore SA1401
// Size of a pointer in bytes
private static readonly long SizeOfPointer = Environment.Is64BitProcess ? 8 : 4;
// Size of a WeakReference<T> in bytes
private static readonly long SizeOfWeakReference = Environment.Is64BitProcess ? 16 : 32;
// Educated guess about the size of an Item in memory (see comments on constructor).
// 3 * SizeOfPointer + total size of fields, rounded up to a multiple of 16.
//
// Computed as follows:
//
// * for 32-bit:
// - initialize count to 3 (number of "hidden" pointers that compose the object header)
// - for every field / auto property, in order of declaration:
// - increment count by 1 for reference types, 2 for long and DateTime
// (as of time of writing there are no fields of other types here)
// - increment again by 1 if this field "weighs" 1 and the next one "weighs" 2
// (padding for field alignment)
// - multiply count by 4 (size of a pointer)
// - if the result is not a multiple of 16, round it up to next multiple of 16
//
// * for 64-bit:
// - initialize count to 3 (number of "hidden" pointers that compose the object header)
// - for every field / auto property, in order of declaration, increment count by 1
// (at the time of writing there are no fields here that need padding on 64-bit)
// - multiply count by 8 (size of a pointer)
// - if the result is not a multiple of 16, round it up to next multiple of 16
private static readonly long SizeOfItem = Environment.Is64BitProcess ? 96 : 128;
private readonly object _syncRoot = new object();
// Used to update total size of section.
// Weak reference avoids circularity.
private readonly WeakReference<FileCache.Section> _section;
// There are only 3 possible compression methods,
// hence a dictionary (or two dictionaries) would be overkill.
private byte[]? _uncompressedContent;
private byte[]? _gzippedContent;
private byte[]? _deflatedContent;
internal FileCacheItem(FileCache.Section section, DateTime lastModifiedUtc, long length)
{
_section = new WeakReference<FileCache.Section>(section);
LastModifiedUtc = lastModifiedUtc;
Length = length;
// There is no way to know the actual size of an object at runtime.
// This method makes some educated guesses, based on the following
// article (among others):
// https://codingsight.com/precise-computation-of-clr-object-size/
// PreviousKey and NextKey values aren't counted in
// because they are just references to existing strings.
SizeInCache = SizeOfItem + SizeOfWeakReference;
}
public DateTime LastModifiedUtc { get; }
public long Length { get; }
// This is the (approximate) in-memory size of this object.
// It is NOT the length of the cache resource!
public long SizeInCache { get; private set; }
public byte[]? GetContent(CompressionMethod compressionMethod)
{
// If there are both entity tag and content, use them.
switch (compressionMethod)
{
case CompressionMethod.Deflate:
if (_deflatedContent != null) return _deflatedContent;
break;
case CompressionMethod.Gzip:
if (_gzippedContent != null) return _gzippedContent;
break;
default:
if (_uncompressedContent != null) return _uncompressedContent;
break;
}
// Try to convert existing content, if any.
byte[]? content;
if (_uncompressedContent != null)
{
content = CompressionUtility.ConvertCompression(_uncompressedContent, CompressionMethod.None, compressionMethod);
}
else if (_gzippedContent != null)
{
content = CompressionUtility.ConvertCompression(_gzippedContent, CompressionMethod.Gzip, compressionMethod);
}
else if (_deflatedContent != null)
{
content = CompressionUtility.ConvertCompression(_deflatedContent, CompressionMethod.Deflate, compressionMethod);
}
else
{
// No content whatsoever.
return null;
}
return SetContent(compressionMethod, content);
}
public byte[]? SetContent(CompressionMethod compressionMethod, byte[]? content)
{
// This is the bare minimum locking we need
// to ensure we don't mess sizes up.
byte[]? oldContent;
lock (_syncRoot)
{
switch (compressionMethod)
{
case CompressionMethod.Deflate:
oldContent = _deflatedContent;
_deflatedContent = content;
break;
case CompressionMethod.Gzip:
oldContent = _gzippedContent;
_gzippedContent = content;
break;
default:
oldContent = _uncompressedContent;
_uncompressedContent = content;
break;
}
}
var sizeDelta = GetSizeOf(content) - GetSizeOf(oldContent);
SizeInCache += sizeDelta;
if (_section.TryGetTarget(out var section))
section.UpdateTotalSize(sizeDelta);
return content;
}
// Round up to a multiple of 16
private static long RoundUpTo16(long n)
{
var remainder = n % 16;
return remainder > 0 ? n + (16 - remainder) : n;
}
// The size of a byte array is 3 * SizeOfPointer + 1 (size of byte) * Length
private static long GetSizeOf(byte[]? arr) => arr == null ? 0 : RoundUpTo16(3 * SizeOfPointer) + arr.Length;
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using EmbedIO.Utilities;
using Swan;
namespace EmbedIO.Files.Internal
{
internal class HtmlDirectoryLister : IDirectoryLister
{
private static readonly Lazy<IDirectoryLister> LazyInstance = new Lazy<IDirectoryLister>(() => new HtmlDirectoryLister());
private HtmlDirectoryLister()
{
}
public static IDirectoryLister Instance => LazyInstance.Value;
public string ContentType { get; } = MimeType.Html + "; encoding=" + WebServer.DefaultEncoding.WebName;
public async Task ListDirectoryAsync(
MappedResourceInfo info,
string absoluteUrlPath,
IEnumerable<MappedResourceInfo> entries,
Stream stream,
CancellationToken cancellationToken)
{
const int MaxEntryLength = 50;
const int SizeIndent = -20; // Negative for right alignment
if (!info.IsDirectory)
throw SelfCheck.Failure($"{nameof(HtmlDirectoryLister)}.{nameof(ListDirectoryAsync)} invoked with a file, not a directory.");
var encodedPath = WebUtility.HtmlEncode(absoluteUrlPath);
using var text = new StreamWriter(stream, WebServer.DefaultEncoding);
text.Write("<html><head><title>Index of ");
text.Write(encodedPath);
text.Write("</title></head><body><h1>Index of ");
text.Write(encodedPath);
text.Write("</h1><hr/><pre>");
if (encodedPath.Length > 1)
text.Write("<a href='../'>../</a>\n");
entries = entries.ToArray();
foreach (var directory in entries.Where(m => m.IsDirectory).OrderBy(e => e.Name))
{
text.Write($"<a href=\"{Uri.EscapeDataString(directory.Name)}\">{WebUtility.HtmlEncode(directory.Name)}</a>");
text.Write(new string(' ', Math.Max(1, MaxEntryLength - directory.Name.Length + 1)));
text.Write(HttpDate.Format(directory.LastModifiedUtc));
text.Write('\n');
await Task.Yield();
}
foreach (var file in entries.Where(m => m.IsFile).OrderBy(e => e.Name))
{
text.Write($"<a href=\"{Uri.EscapeDataString(file.Name)}\">{WebUtility.HtmlEncode(file.Name)}</a>");
text.Write(new string(' ', Math.Max(1, MaxEntryLength - file.Name.Length + 1)));
text.Write(HttpDate.Format(file.LastModifiedUtc));
text.Write($" {file.Length.ToString("#,###", CultureInfo.InvariantCulture),SizeIndent}\n");
await Task.Yield();
}
text.Write("</pre><hr/></body></html>");
}
}
}

View File

@@ -0,0 +1,8 @@
namespace EmbedIO.Files.Internal
{
internal static class MappedResourceInfoExtensions
{
public static string GetEntityTag(this MappedResourceInfo @this, CompressionMethod compressionMethod)
=> EntityTag.Compute(@this.LastModifiedUtc, @this.Length, compressionMethod);
}
}

View File

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

View File

@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using EmbedIO.Utilities;
namespace EmbedIO.Files
{
/// <summary>
/// Provides access to embedded resources to a <see cref="FileModule"/>.
/// </summary>
/// <seealso cref="IFileProvider" />
public class ResourceFileProvider : IFileProvider
{
private readonly DateTime _fileTime = DateTime.UtcNow;
/// <summary>
/// Initializes a new instance of the <see cref="ResourceFileProvider"/> class.
/// </summary>
/// <param name="assembly">The assembly where served files are contained as embedded resources.</param>
/// <param name="pathPrefix">A string to prepend to provider-specific paths
/// to form the name of a manifest resource in <paramref name="assembly"/>.</param>
/// <exception cref="ArgumentNullException"><paramref name="assembly"/> is <see langword="null"/>.</exception>
public ResourceFileProvider(Assembly assembly, string pathPrefix)
{
Assembly = Validate.NotNull(nameof(assembly), assembly);
PathPrefix = pathPrefix ?? string.Empty;
}
/// <inheritdoc />
public event Action<string> ResourceChanged
{
add { }
remove { }
}
/// <summary>
/// Gets the assembly where served files are contained as embedded resources.
/// </summary>
public Assembly Assembly { get; }
/// <summary>
/// Gets a string that is prepended to provider-specific paths to form the name of a manifest resource in <see cref="Assembly"/>.
/// </summary>
public string PathPrefix { get; }
/// <inheritdoc />
public bool IsImmutable => true;
/// <inheritdoc />
public void Start(CancellationToken cancellationToken)
{
}
/// <inheritdoc />
public MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider)
{
var resourceName = PathPrefix + urlPath.Replace('/', '.');
long size;
try
{
using var stream = Assembly.GetManifestResourceStream(resourceName);
if (stream == null || stream == Stream.Null)
return null;
size = stream.Length;
}
catch (FileNotFoundException)
{
return null;
}
var lastSlashPos = urlPath.LastIndexOf('/');
var name = urlPath.Substring(lastSlashPos + 1);
return MappedResourceInfo.ForFile(
resourceName,
name,
_fileTime,
size,
mimeTypeProvider.GetMimeType(Path.GetExtension(name)));
}
/// <inheritdoc />
public Stream OpenFile(string path) => Assembly.GetManifestResourceStream(path);
/// <inheritdoc />
public IEnumerable<MappedResourceInfo> GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider)
=> Enumerable.Empty<MappedResourceInfo>();
}
}

View File

@@ -0,0 +1,110 @@
using EmbedIO.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading;
namespace EmbedIO.Files
{
/// <summary>
/// Provides access to files contained in a <c>.zip</c> file to a <see cref="FileModule"/>.
/// </summary>
/// <seealso cref="IFileProvider" />
public class ZipFileProvider : IDisposable, IFileProvider
{
private readonly ZipArchive _zipArchive;
/// <summary>
/// Initializes a new instance of the <see cref="ZipFileProvider"/> class.
/// </summary>
/// <param name="zipFilePath">The zip file path.</param>
public ZipFileProvider(string zipFilePath)
: this(new FileStream(Validate.LocalPath(nameof(zipFilePath), zipFilePath, true), FileMode.Open))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ZipFileProvider"/> class.
/// </summary>
/// <param name="stream">The stream that contains the archive.</param>
/// <param name="leaveOpen"><see langword="true"/> to leave the stream open after the web server
/// is disposed; otherwise, <see langword="false"/>.</param>
public ZipFileProvider(Stream stream, bool leaveOpen = false)
{
_zipArchive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen);
}
/// <summary>
/// Finalizes an instance of the <see cref="ZipFileProvider"/> class.
/// </summary>
~ZipFileProvider()
{
Dispose(false);
}
/// <inheritdoc />
public event Action<string> ResourceChanged
{
add { }
remove { }
}
/// <inheritdoc />
public bool IsImmutable => true;
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public void Start(CancellationToken cancellationToken)
{
}
/// <inheritdoc />
public MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider)
{
if (urlPath.Length == 1)
return null;
urlPath = Uri.UnescapeDataString(urlPath);
var entry = _zipArchive.GetEntry(urlPath.Substring(1));
if (entry == null)
return null;
return MappedResourceInfo.ForFile(
entry.FullName,
entry.Name,
entry.LastWriteTime.DateTime,
entry.Length,
mimeTypeProvider.GetMimeType(Path.GetExtension(entry.Name)));
}
/// <inheritdoc />
public Stream OpenFile(string path)
=> _zipArchive.GetEntry(path)?.Open() ?? throw new FileNotFoundException($"\"{path}\" cannot be found in Zip archive.");
/// <inheritdoc />
public IEnumerable<MappedResourceInfo> GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider)
=> Enumerable.Empty<MappedResourceInfo>();
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources;
/// <see langword="false"/> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!disposing)
return;
_zipArchive.Dispose();
}
}
}

View File

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

View File

@@ -0,0 +1,33 @@
using System;
using System.Net;
using EmbedIO.Utilities;
namespace EmbedIO
{
partial class HttpContextExtensions
{
/// <summary>
/// Sets a redirection status code and adds a <c>Location</c> header to the response.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> interface on which this method is called.</param>
/// <param name="location">The URL to which the user agent should be redirected.</param>
/// <param name="statusCode">The status code to set on the response.</param>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="location"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para><paramref name="location"/> is not a valid relative or absolute URL.<see langword="null"/>.</para>
/// <para>- or -</para>
/// <para><paramref name="statusCode"/> is not a redirection (3xx) status code.</para>
/// </exception>
public static void Redirect(this IHttpContext @this, string location, int statusCode = (int)HttpStatusCode.Found)
{
location = Validate.Url(nameof(location), location, @this.Request.Url);
if (statusCode < 300 || statusCode > 399)
throw new ArgumentException("Redirect status code is not valid.", nameof(statusCode));
@this.Response.SetEmptyResponse(statusCode);
@this.Response.Headers[HttpHeaderNames.Location] = location;
}
}
}

View File

@@ -0,0 +1,61 @@
using System.IO;
using System.IO.Compression;
using System.Text;
using Swan.Logging;
namespace EmbedIO
{
partial class HttpContextExtensions
{
/// <summary>
/// <para>Wraps the request input stream and returns a <see cref="Stream"/> that can be used directly.</para>
/// <para>Decompression of compressed request bodies is implemented if specified in the web server's options.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <returns>
/// <para>A <see cref="Stream"/> that can be used to write response data.</para>
/// <para>This stream MUST be disposed when finished writing.</para>
/// </returns>
/// <seealso cref="OpenRequestText"/>
/// <seealso cref="WebServerOptionsBase.SupportCompressedRequests"/>
public static Stream OpenRequestStream(this IHttpContext @this)
{
var stream = @this.Request.InputStream;
var encoding = @this.Request.Headers[HttpHeaderNames.ContentEncoding]?.Trim();
switch (encoding)
{
case CompressionMethodNames.Gzip:
if (@this.SupportCompressedRequests)
return new GZipStream(stream, CompressionMode.Decompress);
break;
case CompressionMethodNames.Deflate:
if (@this.SupportCompressedRequests)
return new DeflateStream(stream, CompressionMode.Decompress);
break;
case CompressionMethodNames.None:
case null:
return stream;
}
$"[{@this.Id}] Unsupported request content encoding \"{encoding}\", sending 400 Bad Request..."
.Warn(nameof(OpenRequestStream));
throw HttpException.BadRequest($"Unsupported content encoding \"{encoding}\"");
}
/// <summary>
/// <para>Wraps the request input stream and returns a <see cref="TextReader" /> that can be used directly.</para>
/// <para>Decompression of compressed request bodies is implemented if specified in the web server's options.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpContext" /> on which this method is called.</param>
/// <returns>
/// <para>A <see cref="TextReader" /> that can be used to read the request body as text.</para>
/// <para>This reader MUST be disposed when finished reading.</para>
/// </returns>
/// <seealso cref="OpenRequestStream"/>
/// <seealso cref="WebServerOptionsBase.SupportCompressedRequests"/>
public static TextReader OpenRequestText(this IHttpContext @this)
=> new StreamReader(OpenRequestStream(@this), @this.Request.ContentEncoding);
}
}

View File

@@ -0,0 +1,175 @@
using System;
using System.Collections.Specialized;
using System.IO;
using System.Threading.Tasks;
using EmbedIO.Utilities;
using Swan;
namespace EmbedIO
{
partial class HttpContextExtensions
{
private static readonly object FormDataKey = new object();
private static readonly object QueryDataKey = new object();
/// <summary>
/// Asynchronously retrieves the request body as an array of <see langword="byte"/>s.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <returns>A <see cref="Task{TResult}">Task</see>, representing the ongoing operation,
/// whose result will be an array of <see cref="byte"/>s containing the request body.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
public static async Task<byte[]> GetRequestBodyAsByteArrayAsync(this IHttpContext @this)
{
using var buffer = new MemoryStream();
using var stream = @this.OpenRequestStream();
await stream.CopyToAsync(buffer, WebServer.StreamCopyBufferSize, @this.CancellationToken).ConfigureAwait(false);
return buffer.ToArray();
}
/// <summary>
/// Asynchronously buffers the request body into a read-only <see cref="MemoryStream"/>.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <returns>A <see cref="Task{TResult}">Task</see>, representing the ongoing operation,
/// whose result will be a read-only <see cref="MemoryStream"/> containing the request body.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
public static async Task<MemoryStream> GetRequestBodyAsMemoryStreamAsync(this IHttpContext @this)
=> new MemoryStream(
await GetRequestBodyAsByteArrayAsync(@this).ConfigureAwait(false),
false);
/// <summary>
/// Asynchronously retrieves the request body as a string.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <returns>A <see cref="Task{TResult}">Task</see>, representing the ongoing operation,
/// whose result will be a <see langword="string"/> representation of the request body.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
public static async Task<string> GetRequestBodyAsStringAsync(this IHttpContext @this)
{
using var reader = @this.OpenRequestText();
return await reader.ReadToEndAsync().ConfigureAwait(false);
}
/// <summary>
/// <para>Asynchronously deserializes a request body, using the default request deserializer.</para>
/// <para>As of EmbedIO version 3.0, the default response serializer has the same behavior of JSON
/// request parsing methods of version 2.</para>
/// </summary>
/// <typeparam name="TData">The expected type of the deserialized data.</typeparam>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <returns>A <see cref="Task{TResult}">Task</see>, representing the ongoing operation,
/// whose result will be the deserialized data.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
public static Task<TData> GetRequestDataAsync<TData>(this IHttpContext @this)
=> RequestDeserializer.Default<TData>(@this);
/// <summary>
/// Asynchronously deserializes a request body, using the specified request deserializer.
/// </summary>
/// <typeparam name="TData">The expected type of the deserialized data.</typeparam>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <param name="deserializer">A <see cref="RequestDeserializerCallback{TData}"/> used to deserialize the request body.</param>
/// <returns>A <see cref="Task{TResult}">Task</see>, representing the ongoing operation,
/// whose result will be the deserialized data.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="deserializer"/> is <see langword="null"/>.</exception>
public static Task<TData> GetRequestDataAsync<TData>(this IHttpContext @this,RequestDeserializerCallback<TData> deserializer)
=> Validate.NotNull(nameof(deserializer), deserializer)(@this);
/// <summary>
/// Asynchronously parses a request body in <c>application/x-www-form-urlencoded</c> format.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <returns>A <see cref="Task{TResult}">Task</see>, representing the ongoing operation,
/// whose result will be a read-only <see cref="NameValueCollection"/>of form field names and values.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>This method may safely be called more than once for the same <see cref="IHttpContext"/>:
/// it will return the same collection instead of trying to parse the request body again.</para>
/// </remarks>
public static async Task<NameValueCollection> GetRequestFormDataAsync(this IHttpContext @this)
{
if (!@this.Items.TryGetValue(FormDataKey, out var previousResult))
{
NameValueCollection result;
try
{
using var reader = @this.OpenRequestText();
result = UrlEncodedDataParser.Parse(await reader.ReadToEndAsync().ConfigureAwait(false), false);
}
catch (Exception e)
{
@this.Items[FormDataKey] = e;
throw;
}
@this.Items[FormDataKey] = result;
return result;
}
switch (previousResult)
{
case NameValueCollection collection:
return collection;
case Exception exception:
throw exception.RethrowPreservingStackTrace();
case null:
throw SelfCheck.Failure($"Previous result of {nameof(HttpContextExtensions)}.{nameof(GetRequestFormDataAsync)} is null.");
default:
throw SelfCheck.Failure($"Previous result of {nameof(HttpContextExtensions)}.{nameof(GetRequestFormDataAsync)} is of unexpected type {previousResult.GetType().FullName}");
}
}
/// <summary>
/// Parses a request URL query. Note that this is different from getting the <see cref="IHttpRequest.QueryString"/> property,
/// in that fields without an equal sign are treated as if they have an empty value, instead of their keys being grouped
/// as values of the <c>null</c> key.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <returns>A read-only <see cref="NameValueCollection"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>This method may safely be called more than once for the same <see cref="IHttpContext"/>:
/// it will return the same collection instead of trying to parse the request body again.</para>
/// </remarks>
public static NameValueCollection GetRequestQueryData(this IHttpContext @this)
{
if (!@this.Items.TryGetValue(QueryDataKey, out var previousResult))
{
NameValueCollection result;
try
{
result = UrlEncodedDataParser.Parse(@this.Request.Url.Query, false);
}
catch (Exception e)
{
@this.Items[FormDataKey] = e;
throw;
}
@this.Items[FormDataKey] = result;
return result;
}
switch (previousResult)
{
case NameValueCollection collection:
return collection;
case Exception exception:
throw exception.RethrowPreservingStackTrace();
case null:
throw SelfCheck.Failure($"Previous result of {nameof(HttpContextExtensions)}.{nameof(GetRequestQueryData)} is null.");
default:
throw SelfCheck.Failure($"Previous result of {nameof(HttpContextExtensions)}.{nameof(GetRequestQueryData)} is of unexpected type {previousResult.GetType().FullName}");
}
}
}
}

View File

@@ -0,0 +1,68 @@
using System.IO;
using System.IO.Compression;
using System.Text;
using EmbedIO.Internal;
namespace EmbedIO
{
partial class HttpContextExtensions
{
/// <summary>
/// <para>Wraps the response output stream and returns a <see cref="Stream"/> that can be used directly.</para>
/// <para>Optional buffering is applied, so that the response may be sent as one instead of using chunked transfer.</para>
/// <para>Proactive negotiation is performed to select the best compression method supported by the client.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> on which this method is called.</param>
/// <param name="buffered">If set to <see langword="true"/>, sent data is collected
/// in a <see cref="MemoryStream"/> and sent all at once when the returned <see cref="Stream"/>
/// is disposed; if set to <see langword="false"/> (the default), chunked transfer will be used.</param>
/// <param name="preferCompression"><see langword="true"/> if sending compressed data is preferred over
/// sending non-compressed data; otherwise, <see langword="false"/>.</param>
/// <returns>
/// <para>A <see cref="Stream"/> that can be used to write response data.</para>
/// <para>This stream MUST be disposed when finished writing.</para>
/// </returns>
/// <seealso cref="OpenResponseText"/>
public static Stream OpenResponseStream(this IHttpContext @this, bool buffered = false, bool preferCompression = true)
{
// No need to check whether negotiation is successful;
// the returned callback will throw HttpNotAcceptableException if it was not.
_ = @this.Request.TryNegotiateContentEncoding(preferCompression, out var compressionMethod, out var prepareResponse);
prepareResponse(@this.Response);
var stream = buffered ? new BufferingResponseStream(@this.Response) : @this.Response.OutputStream;
return compressionMethod switch {
CompressionMethod.Gzip => new GZipStream(stream, CompressionMode.Compress),
CompressionMethod.Deflate => new DeflateStream(stream, CompressionMode.Compress),
_ => stream
};
}
/// <summary>
/// <para>Wraps the response output stream and returns a <see cref="TextWriter" /> that can be used directly.</para>
/// <para>Optional buffering is applied, so that the response may be sent as one instead of using chunked transfer.</para>
/// <para>Proactive negotiation is performed to select the best compression method supported by the client.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpContext" /> on which this method is called.</param>
/// <param name="encoding">
/// <para>The <see cref="Encoding"/> to use to convert text to data bytes.</para>
/// <para>If <see langword="null"/> (the default), <see cref="WebServer.DefaultEncoding"/> (UTF-8 without a byte order mark) is used.</para>
/// </param>
/// <param name="buffered">If set to <see langword="true" />, sent data is collected
/// in a <see cref="MemoryStream" /> and sent all at once when the returned <see cref="Stream" />
/// is disposed; if set to <see langword="false" /> (the default), chunked transfer will be used.</param>
/// <param name="preferCompression"><see langword="true"/> if sending compressed data is preferred over
/// sending non-compressed data; otherwise, <see langword="false"/>.</param>
/// <returns>
/// <para>A <see cref="TextWriter" /> that can be used to write response data.</para>
/// <para>This writer MUST be disposed when finished writing.</para>
/// </returns>
/// <seealso cref="OpenResponseStream"/>
public static TextWriter OpenResponseText(this IHttpContext @this, Encoding? encoding = null, bool buffered = false, bool preferCompression = true)
{
encoding ??= WebServer.DefaultEncoding;
@this.Response.ContentEncoding = encoding;
return new StreamWriter(OpenResponseStream(@this, buffered, preferCompression), encoding);
}
}
}

View File

@@ -0,0 +1,124 @@
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using EmbedIO.Utilities;
namespace EmbedIO
{
partial class HttpContextExtensions
{
private const string StandardHtmlHeaderFormat = "<html><head><meta charset=\"{2}\"><title>{0} - {1}</title></head><body><h1>{0} - {1}</h1>";
private const string StandardHtmlFooter = "</body></html>";
/// <summary>
/// Asynchronously sends a string as response.
/// </summary>
/// <param name="this">The <see cref="IHttpResponse"/> interface on which this method is called.</param>
/// <param name="content">The response content.</param>
/// <param name="contentType">The MIME type of the content. If <see langword="null"/>, the content type will not be set.</param>
/// <param name="encoding">The <see cref="Encoding"/> to use.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException">
/// <para><paramref name="content"/> is <see langword="null"/>.</para>
/// <para>- or -</para>
/// <para><paramref name="encoding"/> is <see langword="null"/>.</para>
/// </exception>
public static async Task SendStringAsync(
this IHttpContext @this,
string content,
string contentType,
Encoding encoding)
{
content = Validate.NotNull(nameof(content), content);
encoding = Validate.NotNull(nameof(encoding), encoding);
if (contentType != null)
{
@this.Response.ContentType = contentType;
@this.Response.ContentEncoding = encoding;
}
using var text = @this.OpenResponseText(encoding);
await text.WriteAsync(content).ConfigureAwait(false);
}
/// <summary>
/// Asynchronously sends a standard HTML response for the specified status code.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> interface on which this method is called.</param>
/// <param name="statusCode">The HTTP status code of the response.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">There is no standard status description for <paramref name="statusCode"/>.</exception>
/// <seealso cref="SendStandardHtmlAsync(IHttpContext,int,Action{TextWriter})"/>
public static Task SendStandardHtmlAsync(this IHttpContext @this, int statusCode)
=> SendStandardHtmlAsync(@this, statusCode, null);
/// <summary>
/// Asynchronously sends a standard HTML response for the specified status code.
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> interface on which this method is called.</param>
/// <param name="statusCode">The HTTP status code of the response.</param>
/// <param name="writeAdditionalHtml">A callback function that may write additional HTML code
/// to a <see cref="TextWriter"/> representing the response output.
/// If not <see langword="null"/>, the callback is called immediately before closing the HTML <c>body</c> tag.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">There is no standard status description for <paramref name="statusCode"/>.</exception>
/// <seealso cref="SendStandardHtmlAsync(IHttpContext,int)"/>
public static Task SendStandardHtmlAsync(
this IHttpContext @this,
int statusCode,
Action<TextWriter>? writeAdditionalHtml)
{
if (!HttpStatusDescription.TryGet(statusCode, out var statusDescription))
throw new ArgumentException("Status code has no standard description.", nameof(statusCode));
@this.Response.StatusCode = statusCode;
@this.Response.StatusDescription = statusDescription;
@this.Response.ContentType = MimeType.Html;
@this.Response.ContentEncoding = WebServer.DefaultEncoding;
using (var text = @this.OpenResponseText(WebServer.DefaultEncoding))
{
text.Write(StandardHtmlHeaderFormat, statusCode, statusDescription, WebServer.DefaultEncoding.WebName);
writeAdditionalHtml?.Invoke(text);
text.Write(StandardHtmlFooter);
}
return Task.CompletedTask;
}
/// <summary>
/// <para>Asynchronously sends serialized data as a response, using the default response serializer.</para>
/// <para>As of EmbedIO version 3.0, the default response serializer has the same behavior of JSON
/// response methods of version 2.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> interface on which this method is called.</param>
/// <param name="data">The data to serialize.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <seealso cref="SendDataAsync(IHttpContext,ResponseSerializerCallback,object)"/>
/// <seealso cref="ResponseSerializer.Default"/>
public static Task SendDataAsync(this IHttpContext @this, object data)
=> ResponseSerializer.Default(@this, data);
/// <summary>
/// <para>Asynchronously sends serialized data as a response, using the specified response serializer.</para>
/// <para>As of EmbedIO version 3.0, the default response serializer has the same behavior of JSON
/// response methods of version 2.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> interface on which this method is called.</param>
/// <param name="serializer">A <see cref="ResponseSerializerCallback"/> used to prepare the response.</param>
/// <param name="data">The data to serialize.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="serializer"/> is <see langword="null"/>.</exception>
/// <seealso cref="SendDataAsync(IHttpContext,ResponseSerializerCallback,object)"/>
/// <seealso cref="ResponseSerializer.Default"/>
public static Task SendDataAsync(this IHttpContext @this, ResponseSerializerCallback serializer, object data)
=> Validate.NotNull(nameof(serializer), serializer)(@this, data);
}
}

View File

@@ -0,0 +1,30 @@
using System;
using EmbedIO.Utilities;
using Swan;
namespace EmbedIO
{
/// <summary>
/// Provides extension methods for types implementing <see cref="IHttpContext"/>.
/// </summary>
public static partial class HttpContextExtensions
{
/// <summary>
/// <para>Gets the underlying <see cref="IHttpContextImpl"/> interface of an <see cref="IHttpContext"/>.</para>
/// <para>This API mainly supports the EmbedIO infrastructure; it is not intended to be used directly from your code,
/// unless to fulfill very specific needs in the development of plug-ins (modules, etc.) for EmbedIO.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpContext"/> interface on which this method is called.</param>
/// <returns>The underlying <see cref="IHttpContextImpl"/> interface representing
/// the HTTP context implementation.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="this"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="EmbedIOInternalErrorException">
/// <paramref name="this"/> does not implement <see cref="IHttpContextImpl"/>.
/// </exception>
public static IHttpContextImpl GetImplementation(this IHttpContext @this)
=> Validate.NotNull(nameof(@this), @this) as IHttpContextImpl
?? throw SelfCheck.Failure($"{@this.GetType().FullName} does not implement {nameof(IHttpContextImpl)}.");
}
}

View File

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

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

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

View File

@@ -0,0 +1,153 @@
using System;
using System.Net;
using System.Runtime.ExceptionServices;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web;
using EmbedIO.Utilities;
using Swan.Logging;
namespace EmbedIO
{
/// <summary>
/// Provides standard handlers for HTTP exceptions at both module and server level.
/// </summary>
/// <remarks>
/// <para>Where applicable, HTTP exception handlers defined in this class
/// use the <see cref="ExceptionHandler.ContactInformation"/> and
/// <see cref="ExceptionHandler.IncludeStackTraces"/> properties to customize
/// their behavior.</para>
/// </remarks>
/// <seealso cref="IWebServer.OnHttpException"/>
/// <seealso cref="IWebModule.OnHttpException"/>
public static class HttpExceptionHandler
{
/// <summary>
/// <para>Gets the default handler used by <see cref="WebServerBase{TOptions}"/>.</para>
/// <para>This is the same as <see cref="HtmlResponse"/>.</para>
/// </summary>
public static HttpExceptionHandlerCallback Default { get; } = HtmlResponse;
/// <summary>
/// Sends an empty response.
/// </summary>
/// <param name="context">An <see cref="IHttpContext" /> interface representing the context of the request.</param>
/// <param name="httpException">The HTTP exception.</param>
/// <returns>A <see cref="Task" /> representing the ongoing operation.</returns>
#pragma warning disable CA1801 // Unused parameter
public static Task EmptyResponse(IHttpContext context, IHttpException httpException)
#pragma warning restore CA1801
=> Task.CompletedTask;
/// <summary>
/// <para>Sends a HTTP exception's <see cref="IHttpException.Message">Message</see> property
/// as a plain text response.</para>
/// <para>This handler does not use the <see cref="IHttpException.DataObject">DataObject</see> property.</para>
/// </summary>
/// <param name="context">An <see cref="IHttpContext" /> interface representing the context of the request.</param>
/// <param name="httpException">The HTTP exception.</param>
/// <returns>A <see cref="Task" /> representing the ongoing operation.</returns>
public static Task PlainTextResponse(IHttpContext context, IHttpException httpException)
=> context.SendStringAsync(httpException.Message ?? string.Empty, MimeType.PlainText, WebServer.DefaultEncoding);
/// <summary>
/// <para>Sends a response with a HTML payload
/// briefly describing the error, including contact information and/or a stack trace
/// if specified via the <see cref="ExceptionHandler.ContactInformation"/>
/// and <see cref="ExceptionHandler.IncludeStackTraces"/> properties, respectively.</para>
/// <para>This handler does not use the <see cref="IHttpException.DataObject">DataObject</see> property.</para>
/// </summary>
/// <param name="context">An <see cref="IHttpContext" /> interface representing the context of the request.</param>
/// <param name="httpException">The HTTP exception.</param>
/// <returns>A <see cref="Task" /> representing the ongoing operation.</returns>
public static Task HtmlResponse(IHttpContext context, IHttpException httpException)
=> context.SendStandardHtmlAsync(
httpException.StatusCode,
text => {
text.Write(
"<p><strong>Exception type:</strong> {0}<p><strong>Message:</strong> {1}",
WebUtility.HtmlEncode(httpException.GetType().FullName ?? "<unknown>"),
WebUtility.HtmlEncode(httpException.Message));
text.Write("<hr><p>If this error is completely unexpected to you, and you think you should not seeing this page, please contact the server administrator");
if (!string.IsNullOrEmpty(ExceptionHandler.ContactInformation))
text.Write(" ({0})", WebUtility.HtmlEncode(ExceptionHandler.ContactInformation));
text.Write(", informing them of the time this error occurred and the action(s) you performed that resulted in this error.</p>");
if (ExceptionHandler.IncludeStackTraces)
{
text.Write(
"</p><p><strong>Stack trace:</strong></p><br><pre>{0}</pre>",
WebUtility.HtmlEncode(httpException.StackTrace));
}
});
/// <summary>
/// <para>Gets a <see cref="HttpExceptionHandlerCallback" /> that will serialize a HTTP exception's
/// <see cref="IHttpException.DataObject">DataObject</see> property and send it as a JSON response.</para>
/// </summary>
/// <param name="serializerCallback">A <see cref="ResponseSerializerCallback" /> used to serialize data and send it to the client.</param>
/// <returns>A <see cref="HttpExceptionHandlerCallback" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="serializerCallback"/> is <see langword="null"/>.</exception>
public static HttpExceptionHandlerCallback DataResponse(ResponseSerializerCallback serializerCallback)
{
Validate.NotNull(nameof(serializerCallback), serializerCallback);
return (context, httpException) => serializerCallback(context, httpException.DataObject);
}
/// <summary>
/// <para>Gets a <see cref="HttpExceptionHandlerCallback" /> that will serialize a HTTP exception's
/// <see cref="IHttpException.Message">Message</see> and <see cref="IHttpException.DataObject">DataObject</see> properties
/// and send them as a JSON response.</para>
/// <para>The response will be a JSON object with a <c>message</c> property and a <c>data</c> property.</para>
/// </summary>
/// <param name="serializerCallback">A <see cref="ResponseSerializerCallback" /> used to serialize data and send it to the client.</param>
/// <returns>A <see cref="HttpExceptionHandlerCallback" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="serializerCallback"/> is <see langword="null"/>.</exception>
public static HttpExceptionHandlerCallback FullDataResponse(ResponseSerializerCallback serializerCallback)
{
Validate.NotNull(nameof(serializerCallback), serializerCallback);
return (context, httpException) => serializerCallback(context, new
{
message = httpException.Message,
data = httpException.DataObject,
});
}
internal static async Task Handle(string logSource, IHttpContext context, Exception exception, HttpExceptionHandlerCallback? handler)
{
if (handler == null || !(exception is IHttpException httpException))
{
ExceptionDispatchInfo.Capture(exception).Throw();
return;
}
exception.Log(logSource, $"[{context.Id}] HTTP exception {httpException.StatusCode}");
try
{
context.Response.SetEmptyResponse(httpException.StatusCode);
context.Response.DisableCaching();
httpException.PrepareResponse(context);
await handler(context, httpException)
.ConfigureAwait(false);
}
catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested)
{
throw;
}
catch (HttpListenerException)
{
throw;
}
catch (Exception exception2)
{
exception2.Log(logSource, $"[{context.Id}] Unhandled exception while handling HTTP exception {httpException.StatusCode}");
}
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Threading.Tasks;
namespace EmbedIO
{
/// <summary>
/// A callback used to build the contents of the response for an <see cref="IHttpException" />.
/// </summary>
/// <param name="context">An <see cref="IHttpContext" /> interface representing the context of the request.</param>
/// <param name="httpException">An <see cref="IHttpException" /> interface.</param>
/// <returns>A <see cref="Task" /> representing the ongoing operation.</returns>
/// <remarks>
/// <para>When this delegate is called, the response's status code has already been set and the <see cref="IHttpException.PrepareResponse"/>
/// method has already been called. The only thing left to do is preparing the response's content, according
/// to the <see cref="IHttpException.Message"/> property.</para>
/// <para>Any exception thrown by a handler (even a HTTP exception) will go unhandled: the web server
/// will not crash, but processing of the request will be aborted, and the response will be flushed as-is.
/// In other words, it is not a good ides to <c>throw HttpException.NotFound()</c> (or similar)
/// from a handler.</para>
/// </remarks>
public delegate Task HttpExceptionHandlerCallback(IHttpContext context, IHttpException httpException);
}

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

@@ -0,0 +1,449 @@
namespace EmbedIO
{
/// <summary>
/// Exposes known HTTP header names.
/// </summary>
/// <remarks>
/// <para>The constants in this class have been extracted from a list of known HTTP header names.
/// The presence of a header name in this class is not a guarantee that EmbedIO supports,
/// or even recognizes, it. Refer to the documentation for each module for information about supported
/// headers.</para>
/// </remarks>
public static class HttpHeaderNames
{
// The .NET Core sources were taken as reference for this list of constants.
// See https://github.com/dotnet/corefx/blob/master/src/Common/src/System/Net/HttpKnownHeaderNames.cs
// However, not all constants come from there, so be careful not to copy-paste indiscriminately.
/// <summary>
/// The <c>Accept</c> HTTP header.
/// </summary>
public const string Accept = "Accept";
/// <summary>
/// The <c>Accept-Charset</c> HTTP header.
/// </summary>
public const string AcceptCharset = "Accept-Charset";
/// <summary>
/// The <c>Accept-Encoding</c> HTTP header.
/// </summary>
public const string AcceptEncoding = "Accept-Encoding";
/// <summary>
/// The <c>Accept-Language</c> HTTP header.
/// </summary>
public const string AcceptLanguage = "Accept-Language";
/// <summary>
/// The <c>Accept-Patch</c> HTTP header.
/// </summary>
public const string AcceptPatch = "Accept-Patch";
/// <summary>
/// The <c>Accept-Ranges</c> HTTP header.
/// </summary>
public const string AcceptRanges = "Accept-Ranges";
/// <summary>
/// The <c>Access-Control-Allow-Credentials</c> HTTP header.
/// </summary>
public const string AccessControlAllowCredentials = "Access-Control-Allow-Credentials";
/// <summary>
/// The <c>Access-Control-Allow-Headers</c> HTTP header.
/// </summary>
public const string AccessControlAllowHeaders = "Access-Control-Allow-Headers";
/// <summary>
/// The <c>Access-Control-Allow-Methods</c> HTTP header.
/// </summary>
public const string AccessControlAllowMethods = "Access-Control-Allow-Methods";
/// <summary>
/// The <c>Access-Control-Allow-Origin</c> HTTP header.
/// </summary>
public const string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
/// <summary>
/// The <c>Access-Control-Expose-Headers</c> HTTP header.
/// </summary>
public const string AccessControlExposeHeaders = "Access-Control-Expose-Headers";
/// <summary>
/// The <c>Access-Control-Max-Age</c> HTTP header.
/// </summary>
public const string AccessControlMaxAge = "Access-Control-Max-Age";
/// <summary>
/// The <c>Access-Control-Request-Headers</c> HTTP header.
/// </summary>
public const string AccessControlRequestHeaders = "Access-Control-Request-Headers";
/// <summary>
/// The <c>Access-Control-Request-Method</c> HTTP header.
/// </summary>
public const string AccessControlRequestMethod = "Access-Control-Request-Method";
/// <summary>
/// The <c>Age</c> HTTP header.
/// </summary>
public const string Age = "Age";
/// <summary>
/// The <c>Allow</c> HTTP header.
/// </summary>
public const string Allow = "Allow";
/// <summary>
/// The <c>Alt-Svc</c> HTTP header.
/// </summary>
public const string AltSvc = "Alt-Svc";
/// <summary>
/// The <c>Authorization</c> HTTP header.
/// </summary>
public const string Authorization = "Authorization";
/// <summary>
/// The <c>Cache-Control</c> HTTP header.
/// </summary>
public const string CacheControl = "Cache-Control";
/// <summary>
/// The <c>Connection</c> HTTP header.
/// </summary>
public const string Connection = "Connection";
/// <summary>
/// The <c>Content-Disposition</c> HTTP header.
/// </summary>
public const string ContentDisposition = "Content-Disposition";
/// <summary>
/// The <c>Content-Encoding</c> HTTP header.
/// </summary>
public const string ContentEncoding = "Content-Encoding";
/// <summary>
/// The <c>Content-Language</c> HTTP header.
/// </summary>
public const string ContentLanguage = "Content-Language";
/// <summary>
/// The <c>Content-Length</c> HTTP header.
/// </summary>
public const string ContentLength = "Content-Length";
/// <summary>
/// The <c>Content-Location</c> HTTP header.
/// </summary>
public const string ContentLocation = "Content-Location";
/// <summary>
/// The <c>Content-MD5</c> HTTP header.
/// </summary>
public const string ContentMD5 = "Content-MD5";
/// <summary>
/// The <c>Content-Range</c> HTTP header.
/// </summary>
public const string ContentRange = "Content-Range";
/// <summary>
/// The <c>Content-Security-Policy</c> HTTP header.
/// </summary>
public const string ContentSecurityPolicy = "Content-Security-Policy";
/// <summary>
/// The <c>Content-Type</c> HTTP header.
/// </summary>
public const string ContentType = "Content-Type";
/// <summary>
/// The <c>Cookie</c> HTTP header.
/// </summary>
public const string Cookie = "Cookie";
/// <summary>
/// The <c>Cookie2</c> HTTP header.
/// </summary>
public const string Cookie2 = "Cookie2";
/// <summary>
/// The <c>Date</c> HTTP header.
/// </summary>
public const string Date = "Date";
/// <summary>
/// The <c>ETag</c> HTTP header.
/// </summary>
public const string ETag = "ETag";
/// <summary>
/// The <c>Expect</c> HTTP header.
/// </summary>
public const string Expect = "Expect";
/// <summary>
/// The <c>Expires</c> HTTP header.
/// </summary>
public const string Expires = "Expires";
/// <summary>
/// The <c>From</c> HTTP header.
/// </summary>
public const string From = "From";
/// <summary>
/// The <c>Host</c> HTTP header.
/// </summary>
public const string Host = "Host";
/// <summary>
/// The <c>If-Match</c> HTTP header.
/// </summary>
public const string IfMatch = "If-Match";
/// <summary>
/// The <c>If-Modified-Since</c> HTTP header.
/// </summary>
public const string IfModifiedSince = "If-Modified-Since";
/// <summary>
/// The <c>If-None-Match</c> HTTP header.
/// </summary>
public const string IfNoneMatch = "If-None-Match";
/// <summary>
/// The <c>If-Range</c> HTTP header.
/// </summary>
public const string IfRange = "If-Range";
/// <summary>
/// The <c>If-Unmodified-Since</c> HTTP header.
/// </summary>
public const string IfUnmodifiedSince = "If-Unmodified-Since";
/// <summary>
/// The <c>Keep-Alive</c> HTTP header.
/// </summary>
public const string KeepAlive = "Keep-Alive";
/// <summary>
/// The <c>Last-Modified</c> HTTP header.
/// </summary>
public const string LastModified = "Last-Modified";
/// <summary>
/// The <c>Link</c> HTTP header.
/// </summary>
public const string Link = "Link";
/// <summary>
/// The <c>Location</c> HTTP header.
/// </summary>
public const string Location = "Location";
/// <summary>
/// The <c>Max-Forwards</c> HTTP header.
/// </summary>
public const string MaxForwards = "Max-Forwards";
/// <summary>
/// The <c>Origin</c> HTTP header.
/// </summary>
public const string Origin = "Origin";
/// <summary>
/// The <c>P3P</c> HTTP header.
/// </summary>
public const string P3P = "P3P";
/// <summary>
/// The <c>Pragma</c> HTTP header.
/// </summary>
public const string Pragma = "Pragma";
/// <summary>
/// The <c>Proxy-Authenticate</c> HTTP header.
/// </summary>
public const string ProxyAuthenticate = "Proxy-Authenticate";
/// <summary>
/// The <c>Proxy-Authorization</c> HTTP header.
/// </summary>
public const string ProxyAuthorization = "Proxy-Authorization";
/// <summary>
/// The <c>Proxy-Connection</c> HTTP header.
/// </summary>
public const string ProxyConnection = "Proxy-Connection";
/// <summary>
/// The <c>Public-Key-Pins</c> HTTP header.
/// </summary>
public const string PublicKeyPins = "Public-Key-Pins";
/// <summary>
/// The <c>Range</c> HTTP header.
/// </summary>
public const string Range = "Range";
/// <summary>
/// The <c>Referer</c> HTTP header.
/// </summary>
/// <remarks>
/// <para>The incorrect spelling ("Referer" instead of "Referrer") is intentional
/// and has historical reasons.</para>
/// <para>See the "Etymology" section of <a href="https://en.wikipedia.org/wiki/HTTP_referer">the Wikipedia article</a>
/// on this header for more information.</para>
/// </remarks>
public const string Referer = "Referer";
/// <summary>
/// The <c>Retry-After</c> HTTP header.
/// </summary>
public const string RetryAfter = "Retry-After";
/// <summary>
/// The <c>Sec-WebSocket-Accept</c> HTTP header.
/// </summary>
public const string SecWebSocketAccept = "Sec-WebSocket-Accept";
/// <summary>
/// The <c>Sec-WebSocket-Extensions</c> HTTP header.
/// </summary>
public const string SecWebSocketExtensions = "Sec-WebSocket-Extensions";
/// <summary>
/// The <c>Sec-WebSocket-Key</c> HTTP header.
/// </summary>
public const string SecWebSocketKey = "Sec-WebSocket-Key";
/// <summary>
/// The <c>Sec-WebSocket-Protocol</c> HTTP header.
/// </summary>
public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol";
/// <summary>
/// The <c>Sec-WebSocket-Version</c> HTTP header.
/// </summary>
public const string SecWebSocketVersion = "Sec-WebSocket-Version";
/// <summary>
/// The <c>Server</c> HTTP header.
/// </summary>
public const string Server = "Server";
/// <summary>
/// The <c>Set-Cookie</c> HTTP header.
/// </summary>
public const string SetCookie = "Set-Cookie";
/// <summary>
/// The <c>Set-Cookie2</c> HTTP header.
/// </summary>
public const string SetCookie2 = "Set-Cookie2";
/// <summary>
/// The <c>Strict-Transport-Security</c> HTTP header.
/// </summary>
public const string StrictTransportSecurity = "Strict-Transport-Security";
/// <summary>
/// The <c>TE</c> HTTP header.
/// </summary>
public const string TE = "TE";
/// <summary>
/// The <c>TSV</c> HTTP header.
/// </summary>
public const string TSV = "TSV";
/// <summary>
/// The <c>Trailer</c> HTTP header.
/// </summary>
public const string Trailer = "Trailer";
/// <summary>
/// The <c>Transfer-Encoding</c> HTTP header.
/// </summary>
public const string TransferEncoding = "Transfer-Encoding";
/// <summary>
/// The <c>Upgrade</c> HTTP header.
/// </summary>
public const string Upgrade = "Upgrade";
/// <summary>
/// The <c>Upgrade-Insecure-Requests</c> HTTP header.
/// </summary>
public const string UpgradeInsecureRequests = "Upgrade-Insecure-Requests";
/// <summary>
/// The <c>User-Agent</c> HTTP header.
/// </summary>
public const string UserAgent = "User-Agent";
/// <summary>
/// The <c>Vary</c> HTTP header.
/// </summary>
public const string Vary = "Vary";
/// <summary>
/// The <c>Via</c> HTTP header.
/// </summary>
public const string Via = "Via";
/// <summary>
/// The <c>WWW-Authenticate</c> HTTP header.
/// </summary>
public const string WWWAuthenticate = "WWW-Authenticate";
/// <summary>
/// The <c>Warning</c> HTTP header.
/// </summary>
public const string Warning = "Warning";
/// <summary>
/// The <c>X-AspNet-Version</c> HTTP header.
/// </summary>
public const string XAspNetVersion = "X-AspNet-Version";
/// <summary>
/// The <c>X-Content-Duration</c> HTTP header.
/// </summary>
public const string XContentDuration = "X-Content-Duration";
/// <summary>
/// The <c>X-Content-Type-Options</c> HTTP header.
/// </summary>
public const string XContentTypeOptions = "X-Content-Type-Options";
/// <summary>
/// The <c>X-Frame-Options</c> HTTP header.
/// </summary>
public const string XFrameOptions = "X-Frame-Options";
/// <summary>
/// The <c>X-MSEdge-Ref</c> HTTP header.
/// </summary>
public const string XMSEdgeRef = "X-MSEdge-Ref";
/// <summary>
/// The <c>X-Powered-By</c> HTTP header.
/// </summary>
public const string XPoweredBy = "X-Powered-By";
/// <summary>
/// The <c>X-Request-ID</c> HTTP header.
/// </summary>
public const string XRequestID = "X-Request-ID";
/// <summary>
/// The <c>X-UA-Compatible</c> HTTP header.
/// </summary>
public const string XUACompatible = "X-UA-Compatible";
}
}

View File

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

View File

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

View File

@@ -0,0 +1,50 @@
using System.Net;
namespace EmbedIO
{
/// <summary>
/// When thrown, breaks the request handling control flow
/// and sends a redirection response to the client.
/// </summary>
#pragma warning disable CA1032 // Implement standard exception constructors - they have no meaning here.
public class HttpRangeNotSatisfiableException : HttpException
#pragma warning restore CA1032
{
/// <summary>
/// Initializes a new instance of the <see cref="HttpRangeNotSatisfiableException"/> class.
/// without specifying a value for the response's <c>Content-Range</c> header.
/// </summary>
public HttpRangeNotSatisfiableException()
: this(null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="HttpRangeNotSatisfiableException"/> class.
/// </summary>
/// <param name="contentLength">The total length of the requested resource, expressed in bytes,
/// or <see langword="null"/> to omit the <c>Content-Range</c> header in the response.</param>
public HttpRangeNotSatisfiableException(long? contentLength)
: base((int)HttpStatusCode.RequestedRangeNotSatisfiable)
{
ContentLength = contentLength;
}
/// <summary>
/// Gets the total content length to be specified
/// on the response's <c>Content-Range</c> header.
/// </summary>
public long? ContentLength { get; }
/// <inheritdoc />
public override void PrepareResponse(IHttpContext context)
{
// RFC 7233, Section 3.1: "When this status code is generated in response
// to a byte-range request, the sender
// SHOULD generate a Content-Range header field specifying
// the current length of the selected representation."
if (ContentLength.HasValue)
context.Response.Headers.Set(HttpHeaderNames.ContentRange, $"bytes */{ContentLength.Value}");
}
}
}

View File

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

View File

@@ -0,0 +1,262 @@
using System;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using EmbedIO.Utilities;
namespace EmbedIO
{
/// <summary>
/// Provides extension methods for types implementing <see cref="IHttpRequest"/>.
/// </summary>
public static class HttpRequestExtensions
{
/// <summary>
/// <para>Returns a string representing the remote IP address and port of an <see cref="IHttpRequest"/> interface.</para>
/// <para>This method can be called even on a <see langword="null"/> interface, or one that has no
/// remote end point, or no remote address; it will always return a non-<see langword="null"/>,
/// non-empty string.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpRequest"/> on which this method is called.</param>
/// <returns>
/// If <paramref name="this"/> is <see langword="null"/>, or its <see cref="IHttpRequest.RemoteEndPoint">RemoteEndPoint</see>
/// is <see langword="null"/>, the string <c>"&lt;null&gt;</c>; otherwise, the remote end point's
/// <see cref="IPEndPoint.Address">Address</see> (or the string <c>"&lt;???&gt;"</c> if it is <see langword="null"/>)
/// followed by a colon and the <see cref="IPEndPoint.Port">Port</see> number.
/// </returns>
public static string SafeGetRemoteEndpointStr(this IHttpRequest @this)
{
var endPoint = @this?.RemoteEndPoint;
return endPoint == null
? "<null>"
: $"{endPoint.Address?.ToString() ?? "<???>"}:{endPoint.Port.ToString(CultureInfo.InvariantCulture)}";
}
/// <summary>
/// <para>Attempts to proactively negotiate a compression method for a response,
/// based on a request's <c>Accept-Encoding</c> header (or lack of it).</para>
/// </summary>
/// <param name="this">The <see cref="IHttpRequest"/> on which this method is called.</param>
/// <param name="preferCompression"><see langword="true"/> if sending compressed data is preferred over
/// sending non-compressed data; otherwise, <see langword="false"/>.</param>
/// <param name="compressionMethod">When this method returns, the compression method to use for the response,
/// if content negotiation is successful. This parameter is passed uninitialized.</param>
/// <param name="prepareResponse">When this method returns, a callback that prepares data in an <see cref="IHttpResponse"/>
/// according to the result of content negotiation. This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if content negotiation is successful;
/// otherwise, <see langword="false"/>.</returns>
/// <remarks>
/// <para>If this method returns <see langword="true"/>, the <paramref name="prepareResponse"/> callback
/// will set appropriate response headers to reflect the results of content negotiation.</para>
/// <para>If this method returns <see langword="false"/>, the <paramref name="prepareResponse"/> callback
/// will throw a <see cref="HttpNotAcceptableException"/> to send a <c>406 Not Acceptable</c> response
/// with the <c>Vary</c> header set to <c>Accept-Encoding</c>,
/// so that the client may know the reason why the request has been rejected.</para>
/// <para>If <paramref name="this"/> has no<c>Accept-Encoding</c> header, this method
/// always returns <see langword="true"/> and sets <paramref name="compressionMethod"/>
/// to <see cref="CompressionMethod.None"/>.</para>
/// </remarks>
/// <seealso cref="HttpNotAcceptableException(string)"/>
public static bool TryNegotiateContentEncoding(
this IHttpRequest @this,
bool preferCompression,
out CompressionMethod compressionMethod,
out Action<IHttpResponse> prepareResponse)
{
var acceptedEncodings = new QValueList(true, @this.Headers.GetValues(HttpHeaderNames.AcceptEncoding));
if (!acceptedEncodings.TryNegotiateContentEncoding(preferCompression, out compressionMethod, out var compressionMethodName))
{
prepareResponse = r => throw HttpException.NotAcceptable(HttpHeaderNames.AcceptEncoding);
return false;
}
prepareResponse = r => {
r.Headers.Add(HttpHeaderNames.Vary, HttpHeaderNames.AcceptEncoding);
r.Headers.Set(HttpHeaderNames.ContentEncoding, compressionMethodName);
};
return true;
}
/// <summary>
/// <para>Checks whether an <c>If-None-Match</c> header exists in a request
/// and, if so, whether it contains a given entity tag.</para>
/// <para>See <see href="https://tools.ietf.org/html/rfc7232#section-3.2">RFC7232, Section 3.2</see>
/// for a normative reference; however, see the Remarks section for more information
/// about the RFC compliance of this method.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpRequest"/> on which this method is called.</param>
/// <param name="entityTag">The entity tag.</param>
/// <param name="headerExists">When this method returns, a value that indicates whether an
/// <c>If-None-Match</c> header is present in <paramref name="this"/>, regardless of the method's
/// return value. This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if an <c>If-None-Match</c> header is present in
/// <paramref name="this"/> and one of the entity tags listed in it is equal to <paramref name="entityTag"/>;
/// <see langword="false"/> otherwise.</returns>
/// <remarks>
/// <para><see href="https://tools.ietf.org/html/rfc7232#section-3.2">RFC7232, Section 3.2</see>
/// states that a weak comparison function (as defined in
/// <see href="https://tools.ietf.org/html/rfc7232#section-2.3.2">RFC7232, Section 2.3.2</see>)
/// must be used for <c>If-None-Match</c>. That would mean parsing every entity tag, at least minimally,
/// to determine whether it is a "weak" or "strong" tag. Since EmbedIO currently generates only
/// "strong" tags, this method uses the default string comparer instead.</para>
/// <para>The behavior of this method is thus not, strictly speaking, RFC7232-compliant;
/// it works, though, with entity tags generated by EmbedIO.</para>
/// </remarks>
public static bool CheckIfNoneMatch(this IHttpRequest @this, string entityTag, out bool headerExists)
{
var values = @this.Headers.GetValues(HttpHeaderNames.IfNoneMatch);
if (values == null)
{
headerExists = false;
return false;
}
headerExists = true;
return values.Select(t => t.Trim()).Contains(entityTag);
}
// Check whether the If-Modified-Since request header exists
// and specifies a date and time more recent than or equal to
// the date and time of last modification of the requested resource.
// RFC7232, Section 3.3
/// <summary>
/// <para>Checks whether an <c>If-Modified-Since</c> header exists in a request
/// and, if so, whether its value is a date and time more recent or equal to
/// a given <see cref="DateTime"/>.</para>
/// <para>See <see href="https://tools.ietf.org/html/rfc7232#section-3.3">RFC7232, Section 3.3</see>
/// for a normative reference.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpRequest"/> on which this method is called.</param>
/// <param name="lastModifiedUtc">A date and time value, in Coordinated Universal Time,
/// expressing the last time a resource was modified.</param>
/// <param name="headerExists">When this method returns, a value that indicates whether an
/// <c>If-Modified-Since</c> header is present in <paramref name="this"/>, regardless of the method's
/// return value. This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if an <c>If-Modified-Since</c> header is present in
/// <paramref name="this"/> and its value is a date and time more recent or equal to <paramref name="lastModifiedUtc"/>;
/// <see langword="false"/> otherwise.</returns>
public static bool CheckIfModifiedSince(this IHttpRequest @this, DateTime lastModifiedUtc, out bool headerExists)
{
var value = @this.Headers.Get(HttpHeaderNames.IfModifiedSince);
if (value == null)
{
headerExists = false;
return false;
}
headerExists = true;
return HttpDate.TryParse(value, out var dateTime)
&& dateTime.UtcDateTime >= lastModifiedUtc;
}
// Checks the Range request header to tell whether to send
// a "206 Partial Content" response.
/// <summary>
/// <para>Checks whether a <c>Range</c> header exists in a request
/// and, if so, determines whether it is possible to send a <c>206 Partial Content</c> response.</para>
/// <para>See <see href="https://tools.ietf.org/html/rfc7233">RFC7233</see>
/// for a normative reference; however, see the Remarks section for more information
/// about the RFC compliance of this method.</para>
/// </summary>
/// <param name="this">The <see cref="IHttpRequest"/> on which this method is called.</param>
/// <param name="contentLength">The total length, in bytes, of the response entity, i.e.
/// what would be sent in a <c>200 OK</c> response.</param>
/// <param name="entityTag">An entity tag representing the response entity. This value is checked against
/// the <c>If-Range</c> header, if it is present.</param>
/// <param name="lastModifiedUtc">The date and time value, in Coordinated Universal Time,
/// expressing the last modification time of the resource entity. This value is checked against
/// the <c>If-Range</c> header, if it is present.</param>
/// <param name="start">When this method returns <see langword="true"/>, the start of the requested byte range.
/// This parameter is passed uninitialized.</param>
/// <param name="upperBound">
/// <para>When this method returns <see langword="true"/>, the upper bound of the requested byte range.
/// This parameter is passed uninitialized.</para>
/// <para>Note that the upper bound of a range is NOT the sum of the range's start and length;
/// for example, a range expressed as <c>bytes=0-99</c> has a start of 0, an upper bound of 99,
/// and a length of 100 bytes.</para>
/// </param>
/// <returns>
/// <para>This method returns <see langword="true"/> if the following conditions are satisfied:</para>
/// <list type="bullet">
/// <item><description>>the request's HTTP method is <c>GET</c>;</description></item>
/// <item><description>>a <c>Range</c> header is present in the request;</description></item>
/// <item><description>>either no <c>If-Range</c> header is present in the request, or it
/// specifies an entity tag equal to <paramref name="entityTag"/>, or a UTC date and time
/// equal to <paramref name="lastModifiedUtc"/>;</description></item>
/// <item><description>>the <c>Range</c> header specifies exactly one range;</description></item>
/// <item><description>>the specified range is entirely contained in the range from 0 to <paramref name="contentLength"/> - 1.</description></item>
/// </list>
/// <para>If the last condition is not satisfied, i.e. the specified range start and/or upper bound
/// are out of the range from 0 to <paramref name="contentLength"/> - 1, this method does not return;
/// it throws a <see cref="HttpRangeNotSatisfiableException"/> instead.</para>
/// <para>If any of the other conditions are not satisfied, this method returns <see langword="false"/>.</para>
/// </returns>
/// <remarks>
/// <para>According to <see href="https://tools.ietf.org/html/rfc7233#section-3.1">RFC7233, Section 3.1</see>,
/// there are several conditions under which a server may ignore or reject a range request; therefore,
/// clients are (or should be) prepared to receive a <c>200 OK</c> response with the whole response
/// entity instead of the requested range(s). For this reason, until the generation of
/// <c>multipart/byteranges</c> responses is implemented in EmbedIO, this method will ignore
/// range requests specifying more than one range, even if this behavior is not, strictly speaking,
/// RFC7233-compliant.</para>
/// <para>To make clients aware that range requests are accepted for a resource, every <c>200 OK</c>
/// (or <c>304 Not Modified</c>) response for the same resource should include an <c>Accept-Ranges</c>
/// header with the string <c>bytes</c> as value.</para>
/// </remarks>
public static bool IsRangeRequest(this IHttpRequest @this, long contentLength, string entityTag, DateTime lastModifiedUtc, out long start, out long upperBound)
{
start = 0;
upperBound = contentLength - 1;
// RFC7233, Section 3.1:
// "A server MUST ignore a Range header field received with a request method other than GET."
if (@this.HttpVerb != HttpVerbs.Get)
return false;
// No Range header, no partial content.
var rangeHeader = @this.Headers.Get(HttpHeaderNames.Range);
if (rangeHeader == null)
return false;
// Ignore the Range header if there is no If-Range header
// or if the If-Range header specifies a non-matching validator.
// RFC7233, Section 3.2: "If the validator given in the If-Range header field matches the
// current validator for the selected representation of the target
// resource, then the server SHOULD process the Range header field as
// requested.If the validator does not match, the server MUST ignore
// the Range header field.Note that this comparison by exact match,
// including when the validator is an HTTP-date, differs from the
// "earlier than or equal to" comparison used when evaluating an
// If-Unmodified-Since conditional."
var ifRange = @this.Headers.Get(HttpHeaderNames.IfRange)?.Trim();
if (ifRange != null && ifRange != entityTag)
{
if (!HttpDate.TryParse(ifRange, out var rangeDate))
return false;
if (rangeDate.UtcDateTime != lastModifiedUtc)
return false;
}
// Ignore the Range request header if it cannot be parsed successfully.
if (!RangeHeaderValue.TryParse(rangeHeader, out var range))
return false;
// EmbedIO does not support multipart/byteranges responses (yet),
// thus ignore range requests that specify one range.
if (range.Ranges.Count != 1)
return false;
var firstRange = range.Ranges.First();
start = firstRange.From ?? 0L;
upperBound = firstRange.To ?? contentLength - 1;
if (start >= contentLength || upperBound < start || upperBound >= contentLength)
throw HttpException.RangeNotSatisfiable(contentLength);
return true;
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using EmbedIO.Utilities;
namespace EmbedIO
{
/// <summary>
/// Provides extension methods for types implementing <see cref="IHttpResponse"/>.
/// </summary>
public static class HttpResponseExtensions
{
/// <summary>
/// Sets the necessary headers to disable caching of a response on the client side.
/// </summary>
/// <param name="this">The <see cref="IHttpResponse"/> interface on which this method is called.</param>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
public static void DisableCaching(this IHttpResponse @this)
{
var headers = @this.Headers;
headers.Set(HttpHeaderNames.Expires, "Sat, 26 Jul 1997 05:00:00 GMT");
headers.Set(HttpHeaderNames.LastModified, HttpDate.Format(DateTime.UtcNow));
headers.Set(HttpHeaderNames.CacheControl, "no-store, no-cache, must-revalidate");
headers.Add(HttpHeaderNames.Pragma, "no-cache");
}
/// <summary>
/// Prepares a standard response without a body for the specified status code.
/// </summary>
/// <param name="this">The <see cref="IHttpResponse"/> interface on which this method is called.</param>
/// <param name="statusCode">The HTTP status code of the response.</param>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">There is no standard status description for <paramref name="statusCode"/>.</exception>
public static void SetEmptyResponse(this IHttpResponse @this, int statusCode)
{
if (!HttpStatusDescription.TryGet(statusCode, out var statusDescription))
throw new ArgumentException("Status code has no standard description.", nameof(statusCode));
@this.StatusCode = statusCode;
@this.StatusDescription = statusDescription;
@this.ContentType = MimeType.Default;
@this.ContentEncoding = null;
}
}
}

View File

@@ -0,0 +1,146 @@
using System.Collections.Generic;
using System.Net;
namespace EmbedIO
{
/// <summary>
/// <para>Provides standard HTTP status descriptions.</para>
/// <para>Data contained in this class comes from the following sources:</para>
/// <list type="bullet">
/// <item><description><see href="https://tools.ietf.org/html/rfc7231#section-6">RFC7231 Section 6</see> (HTTP/1.1 Semantics and Content)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc6585">RFC6585</see> (Additional HTTP Status Codes)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc2774#section-7">RFC2774 Section 7</see> (An HTTP Extension Framework)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc7540#section-9.1.2">RFC7540 Section 9.1.2</see> (HTTP/2)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc4918#section-11">RFC4918 Section 11</see> (WebDAV)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc5842#section-7">RFC5842 Section 7</see> (Binding Extensions to WebDAV)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc7538#section-3">RFC7538 Section 3</see> (HTTP Status Code 308)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc3229#section-10.4.1">RFC3229 Section 10.4.1</see> (Delta encoding in HTTP)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc8297#section-2">RFC8297 Section 2</see> (Early Hints)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc7725#section-3">RFC7725 Section 3</see> (HTTP-status-451)</description></item>
/// <item><description><see href="https://tools.ietf.org/html/rfc2295#section-8.1">RFC2295 Section 8.1</see> (Transparent Content Negotiation)</description></item>
/// </list>
/// </summary>
public static class HttpStatusDescription
{
private static readonly IReadOnlyDictionary<int, string> Dictionary = new Dictionary<int, string> {
{ 100, "Continue" },
{ 101, "Switching Protocols" },
{ 102, "Processing" },
{ 103, "Early Hints" },
{ 200, "OK" },
{ 201, "Created" },
{ 202, "Accepted" },
{ 203, "Non-Authoritative Information" },
{ 204, "No Content" },
{ 205, "Reset Content" },
{ 206, "Partial Content" },
{ 207, "Multi-Status" },
{ 208, "Already Reported" },
{ 226, "IM Used" },
{ 300, "Multiple Choices" },
{ 301, "Moved Permanently" },
{ 302, "Found" },
{ 303, "See Other" },
{ 304, "Not Modified" },
{ 305, "Use Proxy" },
{ 307, "Temporary Redirect" },
{ 308, "Permanent Redirect" },
{ 400, "Bad Request" },
{ 401, "Unauthorized" },
{ 402, "Payment Required" },
{ 403, "Forbidden" },
{ 404, "Not Found" },
{ 405, "Method Not Allowed" },
{ 406, "Not Acceptable" },
{ 407, "Proxy Authentication Required" },
{ 408, "Request Timeout" },
{ 409, "Conflict" },
{ 410, "Gone" },
{ 411, "Length Required" },
{ 412, "Precondition Failed" },
{ 413, "Request Entity Too Large" },
{ 414, "Request-Uri Too Long" },
{ 415, "Unsupported Media Type" },
{ 416, "Requested Range Not Satisfiable" },
{ 417, "Expectation Failed" },
{ 421, "Misdirected Request" },
{ 422, "Unprocessable Entity" },
{ 423, "Locked" },
{ 424, "Failed Dependency" },
{ 426, "Upgrade Required" },
{ 428, "Precondition Required" },
{ 429, "Too Many Requests" },
{ 431, "Request Header Fields Too Large" },
{ 451, "Unavailable For Legal Reasons" },
{ 500, "Internal Server Error" },
{ 501, "Not Implemented" },
{ 502, "Bad Gateway" },
{ 503, "Service Unavailable" },
{ 504, "Gateway Timeout" },
{ 505, "Http Version Not Supported" },
{ 506, "Variant Also Negotiates" },
{ 507, "Insufficient Storage" },
{ 508, "Loop Detected" },
{ 510, "Not Extended" },
{ 511, "Network Authentication Required" },
};
/// <summary>
/// Attempts to get the standard status description for a <see cref="HttpStatusCode"/>.
/// </summary>
/// <param name="code">The HTTP status code for which the standard description
/// is to be retrieved.</param>
/// <param name="description">When this method returns, the standard HTTP status description
/// for the specified <paramref name="code"/> if it was found, or <see langword="null"/>
/// if it was not found. This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if the specified <paramref name="code"/> was found
/// in the list of HTTP status codes for which the standard description is known;
/// otherwise, <see langword="false"/>.</returns>
/// <seealso cref="TryGet(int,out string)"/>
/// <seealso cref="Get(HttpStatusCode)"/>
public static bool TryGet(HttpStatusCode code, out string description) => Dictionary.TryGetValue((int)code, out description);
/// <summary>
/// Attempts to get the standard status description for a HTTP status code
/// specified as an <see langword="int"/>.
/// </summary>
/// <param name="code">The HTTP status code for which the standard description
/// is to be retrieved.</param>
/// <param name="description">When this method returns, the standard HTTP status description
/// for the specified <paramref name="code"/> if it was found, or <see langword="null"/>
/// if it was not found. This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if the specified <paramref name="code"/> was found
/// in the list of HTTP status codes for which the standard description is known;
/// otherwise, <see langword="false"/>.</returns>
/// <seealso cref="TryGet(HttpStatusCode,out string)"/>
/// <seealso cref="Get(int)"/>
public static bool TryGet(int code, out string description) => Dictionary.TryGetValue(code, out description);
/// <summary>
/// Returns the standard status description for a <see cref="HttpStatusCode"/>.
/// </summary>
/// <param name="code">The HTTP status code for which the standard description
/// is to be retrieved.</param>
/// <returns>The standard HTTP status description for the specified <paramref name="code"/>
/// if it was found, or <see langword="null"/> if it was not found.</returns>
public static string Get(HttpStatusCode code)
{
Dictionary.TryGetValue((int)code, out var description);
return description;
}
/// <summary>
/// Returns the standard status description for a HTTP status code
/// specified as an <see langword="int"/>.
/// </summary>
/// <param name="code">The HTTP status code for which the standard description
/// is to be retrieved.</param>
/// <returns>The standard HTTP status description for the specified <paramref name="code"/>
/// if it was found, or <see langword="null"/> if it was not found.</returns>
public static string Get(int code)
{
Dictionary.TryGetValue(code, out var description);
return description;
}
}
}

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

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

View File

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

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

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

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

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

View File

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

161
Vendor/EmbedIO-3.5.2/Net/HttpListener.cs vendored Normal file
View File

@@ -0,0 +1,161 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using EmbedIO.Net.Internal;
namespace EmbedIO.Net
{
/// <summary>
/// The EmbedIO implementation of the standard HTTP Listener class.
///
/// Based on MONO HttpListener class.
/// </summary>
/// <seealso cref="IDisposable" />
public sealed class HttpListener : IHttpListener
{
private readonly SemaphoreSlim _ctxQueueSem = new (0);
private readonly ConcurrentDictionary<string, HttpListenerContext> _ctxQueue;
private readonly ConcurrentDictionary<HttpConnection, object> _connections;
private readonly HttpListenerPrefixCollection _prefixes;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="HttpListener" /> class.
/// </summary>
/// <param name="certificate">The certificate.</param>
public HttpListener(X509Certificate? certificate = null)
{
Certificate = certificate;
_prefixes = new HttpListenerPrefixCollection(this);
_connections = new ConcurrentDictionary<HttpConnection, object>();
_ctxQueue = new ConcurrentDictionary<string, HttpListenerContext>();
}
/// <inheritdoc />
public bool IgnoreWriteExceptions { get; set; } = true;
/// <inheritdoc />
public bool IsListening { get; private set; }
/// <inheritdoc />
public string Name { get; } = "Unosquare HTTP Listener";
/// <inheritdoc />
public List<string> Prefixes => _prefixes.ToList();
/// <summary>
/// Gets the certificate.
/// </summary>
/// <value>
/// The certificate.
/// </value>
internal X509Certificate? Certificate { get; }
/// <inheritdoc />
public void Start()
{
if (IsListening)
{
return;
}
EndPointManager.AddListener(this);
IsListening = true;
}
/// <inheritdoc />
public void Stop()
{
IsListening = false;
Close(false);
}
/// <inheritdoc />
public void AddPrefix(string urlPrefix) => _prefixes.Add(urlPrefix);
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
Close(true);
_ctxQueueSem.Dispose();
_disposed = true;
}
/// <inheritdoc />
public async Task<IHttpContextImpl> GetContextAsync(CancellationToken cancellationToken)
{
while (true)
{
await _ctxQueueSem.WaitAsync(cancellationToken).ConfigureAwait(false);
foreach (var key in _ctxQueue.Keys)
{
if (_ctxQueue.TryRemove(key, out var context))
{
return context;
}
break;
}
}
}
internal void RegisterContext(HttpListenerContext context)
{
if (!_ctxQueue.TryAdd(context.Id, context))
{
throw new InvalidOperationException("Unable to register context");
}
_ = _ctxQueueSem.Release();
}
internal void UnregisterContext(HttpListenerContext context) => _ctxQueue.TryRemove(context.Id, out _);
internal void AddConnection(HttpConnection cnc) => _connections[cnc] = cnc;
internal void RemoveConnection(HttpConnection cnc) => _connections.TryRemove(cnc, out _);
private void Close(bool closeExisting)
{
EndPointManager.RemoveListener(this);
var keys = _connections.Keys;
var connections = new HttpConnection[keys.Count];
keys.CopyTo(connections, 0);
_connections.Clear();
var list = new List<HttpConnection>(connections);
for (var i = list.Count - 1; i >= 0; i--)
{
list[i].Close(true);
}
if (!closeExisting)
{
return;
}
while (!_ctxQueue.IsEmpty)
{
foreach (var key in _ctxQueue.Keys.ToArray())
{
if (_ctxQueue.TryGetValue(key, out var context))
{
context.Connection.Close(true);
}
}
}
}
}
}

View File

@@ -0,0 +1,429 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace EmbedIO.Net.Internal
{
internal sealed class EndPointListener : IDisposable
{
private readonly Dictionary<HttpConnection, HttpConnection> _unregistered;
private readonly IPEndPoint _endpoint;
private readonly Socket _sock;
private Dictionary<ListenerPrefix, HttpListener> _prefixes;
private List<ListenerPrefix>? _unhandled; // unhandled; host = '*'
private List<ListenerPrefix>? _all; // all; host = '+
public EndPointListener(HttpListener listener, IPAddress address, int port, bool secure)
{
Listener = listener;
Secure = secure;
_endpoint = new IPEndPoint(address, port);
_sock = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
if (address.AddressFamily == AddressFamily.InterNetworkV6 && EndPointManager.UseIpv6)
{
_sock.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false);
}
_sock.Bind(_endpoint);
_sock.Listen(500);
var args = new SocketAsyncEventArgs { UserToken = this };
args.Completed += OnAccept;
Socket? dummy = null;
Accept(_sock, args, ref dummy);
_prefixes = new Dictionary<ListenerPrefix, HttpListener>();
_unregistered = new Dictionary<HttpConnection, HttpConnection>();
}
internal HttpListener Listener { get; }
internal bool Secure { get; }
public bool BindContext(HttpListenerContext context)
{
var req = context.Request;
var listener = SearchListener(req.Url, out var prefix);
if (listener == null)
{
return false;
}
context.Listener = listener;
context.Connection.Prefix = prefix;
return true;
}
public void UnbindContext(HttpListenerContext context) => context.Listener.UnregisterContext(context);
public void Dispose()
{
_sock.Dispose();
List<HttpConnection> connections;
lock (_unregistered)
{
// Clone the list because RemoveConnection can be called from Close
connections = new List<HttpConnection>(_unregistered.Keys);
_unregistered.Clear();
}
foreach (var c in connections)
{
c.Dispose();
}
}
public void AddPrefix(ListenerPrefix prefix, HttpListener listener)
{
List<ListenerPrefix>? current;
List<ListenerPrefix> future;
if (prefix.Host == "*")
{
do
{
current = _unhandled;
// TODO: Should we clone the items?
future = current?.ToList() ?? new List<ListenerPrefix>();
prefix.Listener = listener;
AddSpecial(future, prefix);
}
while (Interlocked.CompareExchange(ref _unhandled, future, current) != current);
return;
}
if (prefix.Host == "+")
{
do
{
current = _all;
future = current?.ToList() ?? new List<ListenerPrefix>();
prefix.Listener = listener;
AddSpecial(future, prefix);
}
while (Interlocked.CompareExchange(ref _all, future, current) != current);
return;
}
Dictionary<ListenerPrefix, HttpListener> prefs, p2;
do
{
prefs = _prefixes;
if (prefs.ContainsKey(prefix))
{
if (prefs[prefix] != listener)
{
throw new HttpListenerException(400, $"There is another listener for {prefix}");
}
return;
}
p2 = prefs.ToDictionary(x => x.Key, x => x.Value);
p2[prefix] = listener;
}
while (Interlocked.CompareExchange(ref _prefixes, p2, prefs) != prefs);
}
public void RemovePrefix(ListenerPrefix prefix)
{
List<ListenerPrefix>? current;
List<ListenerPrefix> future;
if (prefix.Host == "*")
{
do
{
current = _unhandled;
future = current?.ToList() ?? new List<ListenerPrefix>();
if (!RemoveSpecial(future, prefix))
{
break; // Prefix not found
}
}
while (Interlocked.CompareExchange(ref _unhandled, future, current) != current);
CheckIfRemove();
return;
}
if (prefix.Host == "+")
{
do
{
current = _all;
future = current?.ToList() ?? new List<ListenerPrefix>();
if (!RemoveSpecial(future, prefix))
{
break; // Prefix not found
}
}
while (Interlocked.CompareExchange(ref _all, future, current) != current);
CheckIfRemove();
return;
}
Dictionary<ListenerPrefix, HttpListener> prefs, p2;
do
{
prefs = _prefixes;
var prefixKey = _prefixes.Keys.FirstOrDefault(p => p.Path == prefix.Path);
if (prefixKey is null)
{
break;
}
p2 = prefs.ToDictionary(x => x.Key, x => x.Value);
_ = p2.Remove(prefixKey);
}
while (Interlocked.CompareExchange(ref _prefixes, p2, prefs) != prefs);
CheckIfRemove();
}
internal void RemoveConnection(HttpConnection conn)
{
lock (_unregistered)
{
_ = _unregistered.Remove(conn);
}
}
private static void Accept(Socket socket, SocketAsyncEventArgs e, ref Socket? accepted)
{
e.AcceptSocket = null;
bool acceptPending;
try
{
acceptPending = socket.AcceptAsync(e);
}
catch
{
try
{
accepted?.Dispose();
}
catch
{
// ignored
}
accepted = null;
return;
}
if (!acceptPending)
{
ProcessAccept(e);
}
}
private static void ProcessAccept(SocketAsyncEventArgs args)
{
Socket? accepted = null;
if (args.SocketError == SocketError.Success)
{
accepted = args.AcceptSocket;
}
var epl = (EndPointListener)args.UserToken;
Accept(epl._sock, args, ref accepted);
if (accepted == null)
{
return;
}
if (epl.Secure && epl.Listener.Certificate == null)
{
accepted.Dispose();
return;
}
HttpConnection conn;
try
{
conn = new HttpConnection(accepted, epl);
}
catch
{
return;
}
lock (epl._unregistered)
{
epl._unregistered[conn] = conn;
}
_ = conn.BeginReadRequest();
}
private static void OnAccept(object sender, SocketAsyncEventArgs e) => ProcessAccept(e);
private static HttpListener? MatchFromList(string path, List<ListenerPrefix>? list, out ListenerPrefix? prefix)
{
prefix = null;
if (list == null)
{
return null;
}
HttpListener? bestMatch = null;
var bestLength = -1;
foreach (var p in list)
{
if (p.Path.Length < bestLength || !path.StartsWith(p.Path, StringComparison.Ordinal))
{
continue;
}
bestLength = p.Path.Length;
bestMatch = p.Listener;
prefix = p;
}
return bestMatch;
}
private static void AddSpecial(ICollection<ListenerPrefix> coll, ListenerPrefix prefix)
{
if (coll == null)
{
return;
}
if (coll.Any(p => p.Path == prefix.Path))
{
throw new HttpListenerException(400, "Prefix already in use.");
}
coll.Add(prefix);
}
private static bool RemoveSpecial(IList<ListenerPrefix> coll, ListenerPrefix prefix)
{
if (coll == null)
{
return false;
}
var c = coll.Count;
for (var i = 0; i < c; i++)
{
if (coll[i].Path != prefix.Path)
{
continue;
}
coll.RemoveAt(i);
return true;
}
return false;
}
private HttpListener? SearchListener(Uri uri, out ListenerPrefix? prefix)
{
prefix = null;
if (uri == null)
{
return null;
}
var host = uri.Host;
var port = uri.Port;
var path = WebUtility.UrlDecode(uri.AbsolutePath);
var pathSlash = path[path.Length - 1] == '/' ? path : path + "/";
HttpListener? bestMatch = null;
var bestLength = -1;
if (!string.IsNullOrEmpty(host))
{
var result = _prefixes;
foreach (var p in result.Keys)
{
if (p.Path.Length < bestLength)
{
continue;
}
if (p.Host != host || p.Port != port)
{
continue;
}
if (!path.StartsWith(p.Path, StringComparison.Ordinal) && !pathSlash.StartsWith(p.Path, StringComparison.Ordinal))
{
continue;
}
bestLength = p.Path.Length;
bestMatch = result[p];
prefix = p;
}
if (bestLength != -1)
{
return bestMatch;
}
}
var list = _unhandled;
bestMatch = MatchFromList(path, list, out prefix);
if (path != pathSlash && bestMatch == null)
{
bestMatch = MatchFromList(pathSlash, list, out prefix);
}
if (bestMatch != null)
{
return bestMatch;
}
list = _all;
bestMatch = MatchFromList(path, list, out prefix);
if (path != pathSlash && bestMatch == null)
{
bestMatch = MatchFromList(pathSlash, list, out prefix);
}
return bestMatch;
}
private void CheckIfRemove()
{
if (_prefixes.Count > 0)
{
return;
}
var list = _unhandled;
if (list != null && list.Count > 0)
{
return;
}
list = _all;
if (list != null && list.Count > 0)
{
return;
}
EndPointManager.RemoveEndPoint(this, _endpoint);
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Linq;
namespace EmbedIO.Net.Internal
{
internal static class HeaderUtility
{
public static string? GetCharset(string? contentType)
=> contentType?
.Split(';')
.Select(p => p.Trim())
.Where(part => part.StartsWith("charset", StringComparison.OrdinalIgnoreCase))
.Select(GetAttributeValue)
.FirstOrDefault();
public static string? GetAttributeValue(string nameAndValue)
{
var idx = nameAndValue.IndexOf('=');
return idx < 0 || idx == nameAndValue.Length - 1 ? null : nameAndValue.Substring(idx + 1).Trim().Unquote();
}
}
}

View File

@@ -0,0 +1,11 @@
namespace EmbedIO.Net.Internal
{
partial class HttpConnection
{
private enum InputState
{
RequestLine,
Headers,
}
}
}

View File

@@ -0,0 +1,12 @@
namespace EmbedIO.Net.Internal
{
partial class HttpConnection
{
private enum LineState
{
None,
Cr,
Lf,
}
}
}

View File

@@ -0,0 +1,416 @@
using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace EmbedIO.Net.Internal
{
internal sealed partial class HttpConnection : IDisposable
{
private const int BufferSize = 8192;
private readonly Timer _timer;
private readonly EndPointListener _epl;
private Socket? _sock;
private MemoryStream? _ms;
private byte[]? _buffer;
private HttpListenerContext _context;
private StringBuilder? _currentLine;
private RequestStream? _iStream;
private ResponseStream? _oStream;
private bool _contextBound;
private int _sTimeout = 90000; // 90k ms for first request, 15k ms from then on
private HttpListener? _lastListener;
private InputState _inputState = InputState.RequestLine;
private LineState _lineState = LineState.None;
private int _position;
private string? _errorMessage;
public HttpConnection(Socket sock, EndPointListener epl)
{
_sock = sock;
_epl = epl;
IsSecure = epl.Secure;
LocalEndPoint = (IPEndPoint) sock.LocalEndPoint;
RemoteEndPoint = (IPEndPoint) sock.RemoteEndPoint;
Stream = new NetworkStream(sock, false);
if (IsSecure)
{
var sslStream = new SslStream(Stream, true);
try
{
sslStream.AuthenticateAsServer(epl.Listener.Certificate);
}
catch
{
CloseSocket();
throw;
}
Stream = sslStream;
}
_timer = new Timer(OnTimeout, null, Timeout.Infinite, Timeout.Infinite);
_context = null!; // Silence warning about uninitialized field - _context will be initialized by the Init method
Init();
}
public int Reuses { get; private set; }
public Stream Stream { get; }
public IPEndPoint LocalEndPoint { get; }
public IPEndPoint RemoteEndPoint { get; }
public bool IsSecure { get; }
public ListenerPrefix? Prefix { get; set; }
public void Dispose()
{
Close(true);
_timer.Dispose();
_sock?.Dispose();
_ms?.Dispose();
_iStream?.Dispose();
_oStream?.Dispose();
Stream?.Dispose();
_lastListener?.Dispose();
}
public async Task BeginReadRequest()
{
_buffer ??= new byte[BufferSize];
try
{
if (Reuses == 1)
{
_sTimeout = 15000;
}
_ = _timer.Change(_sTimeout, Timeout.Infinite);
var data = await Stream.ReadAsync(_buffer, 0, BufferSize).ConfigureAwait(false);
await OnReadInternal(data).ConfigureAwait(false);
}
catch
{
_ = _timer.Change(Timeout.Infinite, Timeout.Infinite);
CloseSocket();
Unbind();
}
}
public RequestStream GetRequestStream(long contentLength)
{
if (_iStream == null)
{
var buffer = _ms.ToArray();
var length = (int) _ms.Length;
_ms = null;
_iStream = new RequestStream(Stream, buffer, _position, length - _position, contentLength);
}
return _iStream;
}
public ResponseStream GetResponseStream() => _oStream ??= new ResponseStream(Stream, _context.HttpListenerResponse, _context.Listener?.IgnoreWriteExceptions ?? true);
internal void SetError(string message) => _errorMessage = message;
internal void ForceClose() => Close(true);
internal void Close(bool forceClose = false)
{
if (_sock != null)
{
_oStream?.Dispose();
_oStream = null;
}
if (_sock == null)
{
return;
}
forceClose = forceClose
|| !_context.Request.KeepAlive
|| _context.Response.Headers["connection"] == "close";
if (!forceClose)
{
if (_context.HttpListenerRequest.FlushInput())
{
Reuses++;
Unbind();
Init();
_ = BeginReadRequest();
return;
}
}
using (var s = _sock)
{
_sock = null;
try
{
s?.Shutdown(SocketShutdown.Both);
}
catch
{
// ignored
}
}
Unbind();
RemoveConnection();
}
private void Init()
{
_contextBound = false;
_iStream = null;
_oStream = null;
Prefix = null;
_ms = new MemoryStream();
_position = 0;
_inputState = InputState.RequestLine;
_lineState = LineState.None;
_context = new HttpListenerContext(this);
}
private void OnTimeout(object unused)
{
CloseSocket();
Unbind();
}
private async Task OnReadInternal(int offset)
{
_ = _timer.Change(Timeout.Infinite, Timeout.Infinite);
// Continue reading until full header is received.
// Especially important for multipart requests when the second part of the header arrives after a tiny delay
// because the web browser has to measure the content length first.
while (true)
{
try
{
await _ms.WriteAsync(_buffer, 0, offset).ConfigureAwait(false);
if (_ms.Length > 32768)
{
Close(true);
return;
}
}
catch
{
CloseSocket();
Unbind();
return;
}
if (offset == 0)
{
CloseSocket();
Unbind();
return;
}
if (ProcessInput(_ms))
{
if (_errorMessage is null)
{
_context.HttpListenerRequest.FinishInitialization();
}
if (_errorMessage != null || !_epl.BindContext(_context))
{
Close(true);
return;
}
var listener = _context.Listener;
if (_lastListener != listener)
{
RemoveConnection();
listener.AddConnection(this);
_lastListener = listener;
}
_contextBound = true;
listener.RegisterContext(_context);
return;
}
offset = await Stream.ReadAsync(_buffer, 0, BufferSize).ConfigureAwait(false);
}
}
private void RemoveConnection()
{
if (_lastListener != null)
{
_lastListener.RemoveConnection(this);
}
else
{
_epl.RemoveConnection(this);
}
}
// true -> done processing
// false -> need more input
private bool ProcessInput(MemoryStream ms)
{
var buffer = ms.ToArray();
var len = (int)ms.Length;
var used = 0;
while (true)
{
if (_errorMessage != null)
{
return true;
}
if (_position >= len)
{
break;
}
string? line;
try
{
line = ReadLine(buffer, _position, len - _position, out used);
_position += used;
}
catch
{
_errorMessage = "Bad request";
return true;
}
if (line == null)
{
break;
}
if (string.IsNullOrEmpty(line))
{
if (_inputState == InputState.RequestLine)
{
continue;
}
_currentLine = null;
return true;
}
if (_inputState == InputState.RequestLine)
{
_context.HttpListenerRequest.SetRequestLine(line);
_inputState = InputState.Headers;
}
else
{
try
{
_context.HttpListenerRequest.AddHeader(line);
}
catch (Exception e)
{
_errorMessage = e.Message;
return true;
}
}
}
if (used == len)
{
ms.SetLength(0);
_position = 0;
}
return false;
}
private string? ReadLine(byte[] buffer, int offset, int len, out int used)
{
_currentLine ??= new StringBuilder(128);
var last = offset + len;
used = 0;
for (var i = offset; i < last && _lineState != LineState.Lf; i++)
{
used++;
var b = buffer[i];
switch (b)
{
case 13:
_lineState = LineState.Cr;
break;
case 10:
_lineState = LineState.Lf;
break;
default:
_ = _currentLine.Append((char)b);
break;
}
}
if (_lineState != LineState.Lf)
{
return null;
}
_lineState = LineState.None;
var result = _currentLine.ToString();
_currentLine.Length = 0;
return result;
}
private void Unbind()
{
if (!_contextBound)
{
return;
}
_epl.UnbindContext(_context);
_contextBound = false;
}
private void CloseSocket()
{
if (_sock == null)
{
return;
}
try
{
_sock.Dispose();
}
finally
{
_sock = null;
}
RemoveConnection();
}
}
}

View File

@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using EmbedIO.Authentication;
using EmbedIO.Internal;
using EmbedIO.Routing;
using EmbedIO.Sessions;
using EmbedIO.Utilities;
using EmbedIO.WebSockets;
using EmbedIO.WebSockets.Internal;
using Swan.Logging;
namespace EmbedIO.Net.Internal
{
// Provides access to the request and response objects used by the HttpListener class.
internal sealed class HttpListenerContext : IHttpContextImpl
{
private readonly Lazy<IDictionary<object, object>> _items = new (() => new Dictionary<object, object>(), true);
private readonly TimeKeeper _ageKeeper = new ();
private readonly Stack<Action<IHttpContext>> _closeCallbacks = new ();
private bool _closed;
internal HttpListenerContext(HttpConnection cnc)
{
Connection = cnc;
HttpListenerRequest = new HttpListenerRequest(this);
User = Auth.NoUser;
HttpListenerResponse = new HttpListenerResponse(this);
Id = UniqueIdGenerator.GetNext();
LocalEndPoint = Request.LocalEndPoint;
RemoteEndPoint = Request.RemoteEndPoint;
Route = RouteMatch.None;
Session = SessionProxy.None;
}
public string Id { get; }
public CancellationToken CancellationToken { get; set; }
public long Age => _ageKeeper.ElapsedTime;
public IPEndPoint LocalEndPoint { get; }
public IPEndPoint RemoteEndPoint { get; }
public IHttpRequest Request => HttpListenerRequest;
public RouteMatch Route { get; set; }
public string RequestedPath => Route.SubPath ?? string.Empty; // It will never be empty, because modules are matched via base routes - this is just to silence a warning.
public IHttpResponse Response => HttpListenerResponse;
public IPrincipal User { get; set; }
public ISessionProxy Session { get; set; }
public bool SupportCompressedRequests { get; set; }
public IDictionary<object, object> Items => _items.Value;
public bool IsHandled { get; private set; }
public MimeTypeProviderStack MimeTypeProviders { get; } = new MimeTypeProviderStack();
internal HttpListenerRequest HttpListenerRequest { get; }
internal HttpListenerResponse HttpListenerResponse { get; }
internal HttpListener? Listener { get; set; }
internal HttpConnection Connection { get; }
public void SetHandled() => IsHandled = true;
public void OnClose(Action<IHttpContext> callback)
{
if (_closed)
{
throw new InvalidOperationException("HTTP context has already been closed.");
}
_closeCallbacks.Push(Validate.NotNull(nameof(callback), callback));
}
public void Close()
{
_closed = true;
// Always close the response stream no matter what.
Response.Close();
foreach (var callback in _closeCallbacks)
{
try
{
callback(this);
}
catch (Exception e)
{
e.Log("HTTP context", $"[{Id}] Exception thrown by a HTTP context close callback.");
}
}
}
public async Task<IWebSocketContext> AcceptWebSocketAsync(
IEnumerable<string> requestedProtocols,
string acceptedProtocol,
int receiveBufferSize,
TimeSpan keepAliveInterval,
CancellationToken cancellationToken)
{
var webSocket = await WebSocket.AcceptAsync(this, acceptedProtocol).ConfigureAwait(false);
return new WebSocketContext(this, WebSocket.SupportedVersion, requestedProtocols, acceptedProtocol, webSocket, cancellationToken);
}
public string GetMimeType(string extension)
=> MimeTypeProviders.GetMimeType(extension);
public bool TryDetermineCompression(string mimeType, out bool preferCompression)
=> MimeTypeProviders.TryDetermineCompression(mimeType, out preferCompression);
}
}

View File

@@ -0,0 +1,30 @@
using System.Collections.Generic;
namespace EmbedIO.Net.Internal
{
internal class HttpListenerPrefixCollection : List<string>
{
private readonly HttpListener _listener;
internal HttpListenerPrefixCollection(HttpListener listener)
{
_listener = listener;
}
public new void Add(string uriPrefix)
{
ListenerPrefix.CheckUri(uriPrefix);
if (Contains(uriPrefix))
{
return;
}
base.Add(uriPrefix);
if (_listener.IsListening)
{
EndPointManager.AddPrefix(uriPrefix, _listener);
}
}
}
}

View File

@@ -0,0 +1,497 @@
using System;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using EmbedIO.Internal;
using EmbedIO.Utilities;
namespace EmbedIO.Net.Internal
{
/// <summary>
/// Represents an HTTP Listener Request.
/// </summary>
internal sealed partial class HttpListenerRequest : IHttpRequest
{
private static readonly byte[] HttpStatus100 = WebServer.DefaultEncoding.GetBytes("HTTP/1.1 100 Continue\r\n\r\n");
private static readonly char[] Separators = { ' ' };
private readonly HttpConnection _connection;
private CookieList? _cookies;
private Stream? _inputStream;
private bool _kaSet;
private bool _keepAlive;
internal HttpListenerRequest(HttpListenerContext context)
{
_connection = context.Connection;
}
/// <summary>
/// Gets the MIME accept types.
/// </summary>
/// <value>
/// The accept types.
/// </value>
public string[] AcceptTypes { get; private set; } = Array.Empty<string>();
/// <inheritdoc />
public Encoding ContentEncoding
{
get
{
if (!HasEntityBody || ContentType == null)
{
return WebServer.DefaultEncoding;
}
var charSet = HeaderUtility.GetCharset(ContentType);
if (string.IsNullOrEmpty(charSet))
{
return WebServer.DefaultEncoding;
}
try
{
return Encoding.GetEncoding(charSet);
}
catch (ArgumentException)
{
return WebServer.DefaultEncoding;
}
}
}
/// <inheritdoc />
public long ContentLength64 => long.TryParse(Headers[HttpHeaderNames.ContentLength], out var val) ? val : 0;
/// <inheritdoc />
public string ContentType => Headers[HttpHeaderNames.ContentType];
/// <inheritdoc />
public ICookieCollection Cookies => _cookies ??= new CookieList();
/// <inheritdoc />
public bool HasEntityBody => ContentLength64 > 0;
/// <inheritdoc />
public NameValueCollection Headers { get; } = new ();
/// <inheritdoc />
public string HttpMethod { get; private set; } = string.Empty;
/// <inheritdoc />
public HttpVerbs HttpVerb { get; private set; }
/// <inheritdoc />
public Stream InputStream => _inputStream ??= ContentLength64 > 0 ? _connection.GetRequestStream(ContentLength64) : Stream.Null;
/// <inheritdoc />
public bool IsAuthenticated => false;
/// <inheritdoc />
public bool IsLocal => LocalEndPoint.Address?.Equals(RemoteEndPoint.Address) ?? true;
/// <inheritdoc />
public bool IsSecureConnection => _connection.IsSecure;
/// <inheritdoc />
public bool KeepAlive
{
get
{
if (!_kaSet)
{
var cnc = Headers.GetValues(HttpHeaderNames.Connection);
_keepAlive = ProtocolVersion < HttpVersion.Version11
? cnc != null && cnc.Length == 1 && string.Compare(cnc[0], "keep-alive", StringComparison.OrdinalIgnoreCase) == 0
: cnc == null || cnc.All(s => string.Compare(s, "close", StringComparison.OrdinalIgnoreCase) != 0);
_kaSet = true;
}
return _keepAlive;
}
}
/// <inheritdoc />
public IPEndPoint LocalEndPoint => _connection.LocalEndPoint;
/// <inheritdoc />
public Version ProtocolVersion { get; private set; } = HttpVersion.Version11;
/// <inheritdoc />
public NameValueCollection QueryString { get; } = new ();
/// <inheritdoc />
public string RawUrl { get; private set; } = string.Empty;
/// <inheritdoc />
public IPEndPoint RemoteEndPoint => _connection.RemoteEndPoint;
/// <inheritdoc />
public Uri Url { get; private set; } = WebServer.NullUri;
/// <inheritdoc />
public Uri? UrlReferrer { get; private set; }
/// <inheritdoc />
public string UserAgent => Headers[HttpHeaderNames.UserAgent];
public string UserHostAddress => LocalEndPoint.ToString();
public string UserHostName => Headers[HttpHeaderNames.Host];
public string[] UserLanguages { get; private set; } = Array.Empty<string>();
/// <inheritdoc />
public bool IsWebSocketRequest
=> HttpVerb == HttpVerbs.Get
&& ProtocolVersion >= HttpVersion.Version11
&& Headers.Contains(HttpHeaderNames.Upgrade, "websocket")
&& Headers.Contains(HttpHeaderNames.Connection, "Upgrade");
internal void SetRequestLine(string req)
{
const string forbiddenMethodChars = "\"(),/:;<=>?@[\\]{}";
var parts = req.Split(Separators, 3);
if (parts.Length != 3)
{
_connection.SetError("Invalid request line (parts).");
return;
}
HttpMethod = parts[0];
foreach (var c in HttpMethod)
{
// See https://tools.ietf.org/html/rfc7230#section-3.2.6
// for the list of allowed characters
if (c < 32 || c >= 127 || forbiddenMethodChars.IndexOf(c) >= 0)
{
_connection.SetError("(Invalid verb)");
return;
}
}
HttpVerb = IsKnownHttpMethod(HttpMethod, out var verb) ? verb : HttpVerbs.Any;
RawUrl = parts[1];
if (parts[2].Length != 8 || !parts[2].StartsWith("HTTP/", StringComparison.Ordinal))
{
_connection.SetError("Invalid request line (missing HTTP version).");
return;
}
try
{
ProtocolVersion = new Version(parts[2].Substring(5));
if (ProtocolVersion.Major < 1)
{
throw new InvalidOperationException();
}
}
catch
{
_connection.SetError("Invalid request line (could not parse HTTP version).");
}
}
internal void FinishInitialization()
{
var host = UserHostName;
if (ProtocolVersion > HttpVersion.Version10 && string.IsNullOrEmpty(host))
{
_connection.SetError("Invalid host name");
return;
}
var rawUri = UriUtility.StringToAbsoluteUri(RawUrl.ToLowerInvariant());
var path = rawUri?.PathAndQuery ?? RawUrl;
if (string.IsNullOrEmpty(host))
{
host = rawUri?.Host ?? UserHostAddress;
}
var colonPos = host.LastIndexOf(':');
var closedSquareBracketPos = host.LastIndexOf(']');
if (colonPos >= 0 && closedSquareBracketPos < colonPos)
{
host = host.Substring(0, colonPos);
}
// var baseUri = $"{(IsSecureConnection ? "https" : "http")}://{host}:{LocalEndPoint.Port}";
var baseUri = $"http://{host}:{LocalEndPoint.Port}";
if (!Uri.TryCreate(baseUri + path, UriKind.Absolute, out var url))
{
_connection.SetError(WebUtility.HtmlEncode($"Invalid url: {baseUri}{path}"));
return;
}
Url = url;
InitializeQueryString(Url.Query);
if (ContentLength64 == 0 && (HttpVerb == HttpVerbs.Post || HttpVerb == HttpVerbs.Put))
{
return;
}
if (string.Compare(Headers["Expect"], "100-continue", StringComparison.OrdinalIgnoreCase) == 0)
{
_connection.GetResponseStream().InternalWrite(HttpStatus100, 0, HttpStatus100.Length);
}
}
internal void AddHeader(string header)
{
var colon = header.IndexOf(':');
if (colon == -1 || colon == 0)
{
_connection.SetError("Bad Request");
return;
}
var name = header.Substring(0, colon).Trim();
var val = header.Substring(colon + 1).Trim();
Headers.Set(name, val);
switch (name.ToLowerInvariant())
{
case "accept-language":
UserLanguages = val.SplitByComma(); // yes, only split with a ','
break;
case "accept":
AcceptTypes = val.SplitByComma(); // yes, only split with a ','
break;
case "content-length":
Headers[HttpHeaderNames.ContentLength] = val.Trim();
if (ContentLength64 < 0)
{
_connection.SetError("Invalid Content-Length.");
}
break;
case "referer":
try
{
UrlReferrer = new Uri(val);
}
catch
{
UrlReferrer = null;
}
break;
case "cookie":
ParseCookies(val);
break;
}
}
// returns true is the stream could be reused.
internal bool FlushInput()
{
if (!HasEntityBody)
{
return true;
}
var length = 2048;
if (ContentLength64 > 0)
{
length = (int)Math.Min(ContentLength64, length);
}
var bytes = new byte[length];
while (true)
{
try
{
if (InputStream.Read(bytes, 0, length) <= 0)
{
return true;
}
}
catch (ObjectDisposedException)
{
_inputStream = null;
return true;
}
catch
{
return false;
}
}
}
// Optimized for the following list of methods:
// "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"
// ***NOTE***: The verb parameter is NOT VALID upon exit if false is returned.
private static bool IsKnownHttpMethod(string method, out HttpVerbs verb)
{
switch (method.Length)
{
case 3:
switch (method[0])
{
case 'G':
verb = HttpVerbs.Get;
return method[1] == 'E' && method[2] == 'T';
case 'P':
verb = HttpVerbs.Put;
return method[1] == 'U' && method[2] == 'T';
default:
verb = HttpVerbs.Any;
return false;
}
case 4:
switch (method[0])
{
case 'H':
verb = HttpVerbs.Head;
return method[1] == 'E' && method[2] == 'A' && method[3] == 'D';
case 'P':
verb = HttpVerbs.Post;
return method[1] == 'O' && method[2] == 'S' && method[3] == 'T';
default:
verb = HttpVerbs.Any;
return false;
}
case 5:
verb = HttpVerbs.Patch;
return method[0] == 'P'
&& method[1] == 'A'
&& method[2] == 'T'
&& method[3] == 'C'
&& method[4] == 'H';
case 6:
verb = HttpVerbs.Delete;
return method[0] == 'D'
&& method[1] == 'E'
&& method[2] == 'L'
&& method[3] == 'E'
&& method[4] == 'T'
&& method[5] == 'E';
case 7:
verb = HttpVerbs.Options;
return method[0] == 'O'
&& method[1] == 'P'
&& method[2] == 'T'
&& method[3] == 'I'
&& method[4] == 'O'
&& method[5] == 'N'
&& method[6] == 'S';
default:
verb = HttpVerbs.Any;
return false;
}
}
private void ParseCookies(string val)
{
_cookies ??= new CookieList();
var cookieStrings = val.SplitByAny(';', ',')
.Where(x => !string.IsNullOrEmpty(x));
Cookie? current = null;
var version = 0;
foreach (var str in cookieStrings)
{
if (str.StartsWith("$Version", StringComparison.Ordinal))
{
version = int.Parse(str.Substring(str.IndexOf('=') + 1).Unquote(), CultureInfo.InvariantCulture);
}
else if (str.StartsWith("$Path", StringComparison.Ordinal) && current != null)
{
current.Path = str.Substring(str.IndexOf('=') + 1).Trim();
}
else if (str.StartsWith("$Domain", StringComparison.Ordinal) && current != null)
{
current.Domain = str.Substring(str.IndexOf('=') + 1).Trim();
}
else if (str.StartsWith("$Port", StringComparison.Ordinal) && current != null)
{
current.Port = str.Substring(str.IndexOf('=') + 1).Trim();
}
else
{
if (current != null)
{
_cookies.Add(current);
}
current = new Cookie();
var idx = str.IndexOf('=');
if (idx > 0)
{
current.Name = str.Substring(0, idx).Trim();
current.Value = str.Substring(idx + 1).Trim();
}
else
{
current.Name = str.Trim();
current.Value = string.Empty;
}
current.Version = version;
}
}
if (current != null)
{
_cookies.Add(current);
}
}
private void InitializeQueryString(string query)
{
if (string.IsNullOrEmpty(query))
{
return;
}
if (query[0] == '?')
{
query = query.Substring(1);
}
var components = query.Split('&');
foreach (var kv in components)
{
var pos = kv.IndexOf('=');
if (pos == -1)
{
QueryString.Add(null, WebUtility.UrlDecode(kv));
}
else
{
var key = WebUtility.UrlDecode(kv.Substring(0, pos));
var val = WebUtility.UrlDecode(kv.Substring(pos + 1));
QueryString.Add(key, val);
}
}
}
}
}

View File

@@ -0,0 +1,411 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using EmbedIO.Utilities;
namespace EmbedIO.Net.Internal
{
/// <summary>
/// Represents an HTTP Listener's response.
/// </summary>
/// <seealso cref="IDisposable" />
internal sealed class HttpListenerResponse : IHttpResponse, IDisposable
{
private readonly HttpConnection _connection;
private readonly HttpListenerRequest _request;
private readonly string _id;
private bool _disposed;
private string _contentType = MimeType.Html; // Same default value as Microsoft's implementation
private CookieList? _cookies;
private bool _keepAlive;
private ResponseStream? _outputStream;
private int _statusCode = 200;
private bool _chunked;
internal HttpListenerResponse(HttpListenerContext context)
{
_request = context.HttpListenerRequest;
_connection = context.Connection;
_id = context.Id;
_keepAlive = context.Request.KeepAlive;
}
/// <inheritdoc />
public Encoding? ContentEncoding { get; set; } = WebServer.DefaultEncoding;
/// <inheritdoc />
/// <exception cref="ObjectDisposedException">This instance has been disposed.</exception>
/// <exception cref="InvalidOperationException">This property is being set and headers were already sent.</exception>
public long ContentLength64
{
get => Headers.ContainsKey(HttpHeaderNames.ContentLength) && long.TryParse(Headers[HttpHeaderNames.ContentLength], out var val) ? val : 0;
set
{
EnsureCanChangeHeaders();
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "Must be >= 0");
}
Headers[HttpHeaderNames.ContentLength] = value.ToString(CultureInfo.InvariantCulture);
}
}
/// <inheritdoc />
/// <exception cref="ObjectDisposedException">This instance has been disposed.</exception>
/// <exception cref="InvalidOperationException">This property is being set and headers were already sent.</exception>
/// <exception cref="ArgumentNullException">This property is being set to <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">This property is being set to the empty string.</exception>
public string ContentType
{
get => _contentType;
set
{
EnsureCanChangeHeaders();
_contentType = Validate.NotNullOrEmpty(nameof(value), value);
}
}
/// <inheritdoc />
public ICookieCollection Cookies => CookieCollection;
/// <inheritdoc />
public WebHeaderCollection Headers { get; } = new WebHeaderCollection();
/// <inheritdoc />
public bool KeepAlive
{
get => _keepAlive;
set
{
EnsureCanChangeHeaders();
_keepAlive = value;
}
}
/// <inheritdoc />
public Stream OutputStream => _outputStream ??= _connection.GetResponseStream();
/// <inheritdoc />
public Version ProtocolVersion => _request.ProtocolVersion;
/// <inheritdoc />
/// <exception cref="ObjectDisposedException">This instance has been disposed.</exception>
/// <exception cref="InvalidOperationException">This property is being set and headers were already sent.</exception>
public bool SendChunked
{
get => _chunked;
set
{
EnsureCanChangeHeaders();
_chunked = value;
}
}
/// <inheritdoc />
/// <exception cref="ObjectDisposedException">This instance has been disposed.</exception>
/// <exception cref="InvalidOperationException">This property is being set and headers were already sent.</exception>
public int StatusCode
{
get => _statusCode;
set
{
EnsureCanChangeHeaders();
if (value < 100 || value > 999)
{
throw new ArgumentOutOfRangeException(nameof(StatusCode), "StatusCode must be between 100 and 999.");
}
_statusCode = value;
StatusDescription = HttpListenerResponseHelper.GetStatusDescription(value);
}
}
/// <inheritdoc />
public string StatusDescription { get; set; } = "OK";
internal CookieList CookieCollection
{
get => _cookies ??= new CookieList();
set => _cookies = value;
}
internal bool HeadersSent { get; set; }
void IDisposable.Dispose() => Close(true);
public void Close()
{
if (!_disposed)
{
Close(false);
}
}
/// <inheritdoc />
public void SetCookie(Cookie cookie)
{
if (cookie == null)
{
throw new ArgumentNullException(nameof(cookie));
}
if (_cookies != null)
{
if (_cookies.Any(c => cookie.Name == c.Name && cookie.Domain == c.Domain && cookie.Path == c.Path))
{
throw new ArgumentException("The cookie already exists.");
}
}
else
{
_cookies = new CookieList();
}
_cookies.Add(cookie);
}
internal MemoryStream SendHeaders(bool closing)
{
if (_contentType != null)
{
var contentTypeValue = _contentType.IndexOf("charset=", StringComparison.Ordinal) == -1 && ContentEncoding is not null
? $"{_contentType}; charset={ContentEncoding.WebName}"
: _contentType;
Headers.Add(HttpHeaderNames.ContentType, contentTypeValue);
}
if (Headers[HttpHeaderNames.Server] == null)
{
Headers.Add(HttpHeaderNames.Server, WebServer.Signature);
}
if (Headers[HttpHeaderNames.Date] == null)
{
Headers.Add(HttpHeaderNames.Date, HttpDate.Format(DateTime.UtcNow));
}
// HTTP did not support chunked transfer encoding before version 1.1;
// besides, there's no point in setting transfer encoding at all without a request body.
if (closing || ProtocolVersion < HttpVersion.Version11)
{
_chunked = false;
}
// Was content length set to a valid value, AND chunked encoding not set?
// Note that this does not mean that a response body _will_ be sent
// as this could be the response to a HEAD request.
var haveContentLength = !_chunked
&& Headers.ContainsKey(HttpHeaderNames.ContentLength)
&& long.TryParse(Headers[HttpHeaderNames.ContentLength], NumberStyles.None, CultureInfo.InvariantCulture, out var contentLength)
&& contentLength >= 0L;
if (!haveContentLength)
{
// Content length could have been set to an invalid value (e.g. "-1")
// so we must either force it to 0, or remove the header completely.
if (closing)
{
// Content length was not explicitly set to a valid value,
// and there is no request body.
Headers[HttpHeaderNames.ContentLength] = "0";
}
else
{
// Content length was not explicitly set to a valid value,
// and we're going to send a request body.
// - Remove possibly invalid Content-Length header
// - Enable chunked transfer encoding for HTTP 1.1
Headers.Remove(HttpHeaderNames.ContentLength);
if (ProtocolVersion >= HttpVersion.Version11)
{
_chunked = true;
}
}
}
if (_chunked)
{
Headers.Add(HttpHeaderNames.TransferEncoding, "chunked");
}
//// Apache forces closing the connection for these status codes:
//// HttpStatusCode.BadRequest 400
//// HttpStatusCode.RequestTimeout 408
//// HttpStatusCode.LengthRequired 411
//// HttpStatusCode.RequestEntityTooLarge 413
//// HttpStatusCode.RequestUriTooLong 414
//// HttpStatusCode.InternalServerError 500
//// HttpStatusCode.ServiceUnavailable 503
var reuses = _connection.Reuses;
var keepAlive = _statusCode switch {
400 => false,
408 => false,
411 => false,
413 => false,
414 => false,
500 => false,
503 => false,
_ => _keepAlive && reuses < 100
};
_keepAlive = keepAlive;
if (keepAlive)
{
Headers.Add(HttpHeaderNames.Connection, "keep-alive");
if (ProtocolVersion >= HttpVersion.Version11)
{
Headers.Add(HttpHeaderNames.KeepAlive, $"timeout=15,max={100 - reuses}");
}
}
else
{
Headers.Add(HttpHeaderNames.Connection, "close");
}
return WriteHeaders();
}
private static void AppendSetCookieHeader(StringBuilder sb, Cookie cookie)
{
if (cookie.Name.Length == 0)
{
return;
}
_ = sb.Append("Set-Cookie: ");
if (cookie.Version > 0)
{
_ = sb.Append("Version=").Append(cookie.Version).Append("; ");
}
_ = sb
.Append(cookie.Name)
.Append('=')
.Append(cookie.Value);
if (cookie.Expires != DateTime.MinValue)
{
_ = sb
.Append("; Expires=")
.Append(HttpDate.Format(cookie.Expires));
}
if (!string.IsNullOrEmpty(cookie.Path))
{
_ = sb.Append("; Path=").Append(QuotedString(cookie, cookie.Path));
}
if (!string.IsNullOrEmpty(cookie.Domain))
{
_ = sb.Append("; Domain=").Append(QuotedString(cookie, cookie.Domain));
}
if (!string.IsNullOrEmpty(cookie.Port))
{
_ = sb.Append("; Port=").Append(cookie.Port);
}
if (cookie.Secure)
{
_ = sb.Append("; Secure");
}
if (cookie.HttpOnly)
{
_ = sb.Append("; HttpOnly");
}
_ = sb.Append("\r\n");
}
private static string QuotedString(Cookie cookie, string value)
=> cookie.Version == 0 || value.IsToken() ? value : "\"" + value.Replace("\"", "\\\"") + "\"";
private void Close(bool force)
{
_disposed = true;
_connection.Close(force);
}
private string GetHeaderData()
{
var sb = new StringBuilder()
.Append("HTTP/")
.Append(ProtocolVersion)
.Append(' ')
.Append(_statusCode)
.Append(' ')
.Append(StatusDescription)
.Append("\r\n");
foreach (var key in Headers.AllKeys.Where(x => x != "Set-Cookie"))
{
_ = sb
.Append(key)
.Append(": ")
.Append(Headers[key])
.Append("\r\n");
}
if (_cookies != null)
{
foreach (var cookie in _cookies)
{
AppendSetCookieHeader(sb, cookie);
}
}
if (Headers.ContainsKey(HttpHeaderNames.SetCookie))
{
foreach (var cookie in CookieList.Parse(Headers[HttpHeaderNames.SetCookie]))
{
AppendSetCookieHeader(sb, cookie);
}
}
return sb.Append("\r\n").ToString();
}
private MemoryStream WriteHeaders()
{
var stream = new MemoryStream();
var data = WebServer.DefaultEncoding.GetBytes(GetHeaderData());
var preamble = WebServer.DefaultEncoding.GetPreamble();
stream.Write(preamble, 0, preamble.Length);
stream.Write(data, 0, data.Length);
_outputStream ??= _connection.GetResponseStream();
// Assumes that the ms was at position 0
stream.Position = preamble.Length;
HeadersSent = true;
return stream;
}
private void EnsureCanChangeHeaders()
{
if (_disposed)
{
throw new ObjectDisposedException(_id);
}
if (HeadersSent)
{
throw new InvalidOperationException("Header values cannot be changed after headers are sent.");
}
}
}
}

View File

@@ -0,0 +1,55 @@
namespace EmbedIO.Net.Internal
{
internal static class HttpListenerResponseHelper
{
internal static string GetStatusDescription(int code) => code switch {
100 => "Continue",
101 => "Switching Protocols",
102 => "Processing",
200 => "OK",
201 => "Created",
202 => "Accepted",
203 => "Non-Authoritative Information",
204 => "No Content",
205 => "Reset Content",
206 => "Partial Content",
207 => "Multi-Status",
300 => "Multiple Choices",
301 => "Moved Permanently",
302 => "Found",
303 => "See Other",
304 => "Not Modified",
305 => "Use Proxy",
307 => "Temporary Redirect",
400 => "Bad Request",
401 => "Unauthorized",
402 => "Payment Required",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
406 => "Not Acceptable",
407 => "Proxy Authentication Required",
408 => "Request Timeout",
409 => "Conflict",
410 => "Gone",
411 => "Length Required",
412 => "Precondition Failed",
413 => "Request Entity Too Large",
414 => "Request-Uri Too Long",
415 => "Unsupported Media Type",
416 => "Requested Range Not Satisfiable",
417 => "Expectation Failed",
422 => "Unprocessable Entity",
423 => "Locked",
424 => "Failed Dependency",
500 => "Internal Server Error",
501 => "Not Implemented",
502 => "Bad Gateway",
503 => "Service Unavailable",
504 => "Gateway Timeout",
505 => "Http Version Not Supported",
507 => "Insufficient Storage",
_ => string.Empty
};
}
}

View File

@@ -0,0 +1,35 @@
using System;
namespace EmbedIO.Net.Internal
{
internal sealed class ListenerPrefix
{
public ListenerPrefix(string uri)
{
var parsedUri = ListenerUri.Parse(uri);
Secure = parsedUri.Secure;
Host = parsedUri.Host;
Port = parsedUri.Port;
Path = parsedUri.Path;
}
public HttpListener? Listener { get; set; }
public bool Secure { get; }
public string Host { get; }
public int Port { get; }
public string Path { get; }
public static void CheckUri(string uri)
{
_ = ListenerUri.Parse(uri);
}
public bool IsValid() => Path.IndexOf('%') == -1 && Path.IndexOf("//", StringComparison.Ordinal) == -1;
public override string ToString() => $"{Host}:{Port} ({(Secure ? "Secure" : "Insecure")}";
}
}

View File

@@ -0,0 +1,91 @@
using System;
namespace EmbedIO.Net.Internal
{
internal class ListenerUri
{
private ListenerUri(bool secure,
string host,
int port,
string path)
{
Secure = secure;
Host = host;
Port = port;
Path = path;
}
public bool Secure { get; private set; }
public string Host { get; private set; }
public int Port { get; private set; }
public string Path { get; private set; }
public static ListenerUri Parse(string uri)
{
bool secure;
int port;
int parsingPosition;
if (uri.StartsWith("http://"))
{
secure = false;
port = 80;
parsingPosition = "http://".Length;
}
else if (uri.StartsWith("https://"))
{
secure = true;
port = 443;
parsingPosition = "https://".Length;
}
else
{
throw new Exception("Only 'http' and 'https' schemes are supported.");
}
var startOfPath = uri.IndexOf('/', parsingPosition);
if (startOfPath == -1)
{
throw new ArgumentException("Path should end in '/'.");
}
var hostWithPort = uri.Substring(parsingPosition, startOfPath - parsingPosition);
var startOfPortWithColon = hostWithPort.LastIndexOf(':');
if (startOfPortWithColon > -1)
{
startOfPortWithColon += parsingPosition;
}
var endOfIpV6 = hostWithPort.LastIndexOf(']');
if (endOfIpV6 > -1)
{
endOfIpV6 += parsingPosition;
}
if (endOfIpV6 > startOfPortWithColon)
{
startOfPortWithColon = -1;
}
if (startOfPortWithColon != -1 && startOfPortWithColon < startOfPath)
{
if (!int.TryParse(uri.Substring(startOfPortWithColon + 1, startOfPath - startOfPortWithColon - 1), out port) || port <= 0 || port >= 65535)
{
throw new ArgumentException("Invalid port.");
}
}
var host = uri.Substring(parsingPosition, (startOfPortWithColon == -1 ? startOfPath : startOfPortWithColon) - parsingPosition);
var path = uri.Substring(startOfPath);
if (!path.EndsWith("/"))
{
throw new ArgumentException("Path should end in '/'.");
}
return new ListenerUri(secure, host, port, path);
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Linq;
using Swan;
namespace EmbedIO.Net.Internal
{
/// <summary>
/// Represents some System.NET custom extensions.
/// </summary>
internal static class NetExtensions
{
internal static byte[] ToByteArray(this ushort value, Endianness order)
{
var bytes = BitConverter.GetBytes(value);
if (!order.IsHostOrder())
{
Array.Reverse(bytes);
}
return bytes;
}
internal static byte[] ToByteArray(this ulong value, Endianness order)
{
var bytes = BitConverter.GetBytes(value);
if (!order.IsHostOrder())
{
Array.Reverse(bytes);
}
return bytes;
}
internal static byte[] ToHostOrder(this byte[] source, Endianness sourceOrder)
=> source.Length < 1 ? source
: sourceOrder.IsHostOrder() ? source
: source.Reverse().ToArray();
// true: !(true ^ true) or !(false ^ false)
// false: !(true ^ false) or !(false ^ true)
private static bool IsHostOrder(this Endianness order)
=> !(BitConverter.IsLittleEndian ^ (order == Endianness.Little));
}
}

View File

@@ -0,0 +1,143 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace EmbedIO.Net.Internal
{
internal class RequestStream : Stream
{
private readonly Stream _stream;
private readonly byte[] _buffer;
private int _offset;
private int _length;
private long _remainingBody;
internal RequestStream(Stream stream, byte[] buffer, int offset, int length, long contentLength = -1)
{
_stream = stream;
_buffer = buffer;
_offset = offset;
_length = length;
_remainingBody = contentLength;
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override void Flush()
{
}
public override int Read([In, Out] byte[] buffer, int offset, int count)
{
// Call FillFromBuffer to check for buffer boundaries even when remaining_body is 0
var nread = FillFromBuffer(buffer, offset, count);
if (nread == -1)
{
// No more bytes available (Content-Length)
return 0;
}
if (nread > 0)
{
return nread;
}
nread = _stream.Read(buffer, offset, count);
if (nread > 0 && _remainingBody > 0)
{
_remainingBody -= nread;
}
return nread;
}
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
// Returns 0 if we can keep reading from the base stream,
// > 0 if we read something from the buffer.
// -1 if we had a content length set and we finished reading that many bytes.
private int FillFromBuffer(byte[] buffer, int off, int count)
{
if (buffer == null)
{
throw new ArgumentNullException(nameof(buffer));
}
if (off < 0)
{
throw new ArgumentOutOfRangeException(nameof(off), "< 0");
}
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count), "< 0");
}
var len = buffer.Length;
if (off > len)
{
throw new ArgumentException("destination offset is beyond array size");
}
if (off > len - count)
{
throw new ArgumentException("Reading would overrun buffer");
}
if (_remainingBody == 0)
{
return -1;
}
if (_length == 0)
{
return 0;
}
var size = Math.Min(_length, count);
if (_remainingBody > 0)
{
size = (int) Math.Min(size, _remainingBody);
}
if (_offset > _buffer.Length - size)
{
size = Math.Min(size, _buffer.Length - _offset);
}
if (size == 0)
{
return 0;
}
Buffer.BlockCopy(_buffer, _offset, buffer, off, size);
_offset += size;
_length -= size;
if (_remainingBody > 0)
{
_remainingBody -= size;
}
return size;
}
}
}

View File

@@ -0,0 +1,190 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace EmbedIO.Net.Internal
{
internal class ResponseStream : Stream
{
private static readonly byte[] CrLf = { 13, 10 };
private readonly object _headersSyncRoot = new ();
private readonly Stream _stream;
private readonly HttpListenerResponse _response;
private readonly bool _ignoreErrors;
private bool _disposed;
private bool _trailerSent;
internal ResponseStream(Stream stream, HttpListenerResponse response, bool ignoreErrors)
{
_response = response;
_ignoreErrors = ignoreErrors;
_stream = stream;
}
/// <inheritdoc />
public override bool CanRead => false;
/// <inheritdoc />
public override bool CanSeek => false;
/// <inheritdoc />
public override bool CanWrite => true;
/// <inheritdoc />
public override long Length => throw new NotSupportedException();
/// <inheritdoc />
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
/// <inheritdoc />
public override void Flush()
{
}
/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(ResponseStream));
}
byte[] bytes;
var ms = GetHeaders(false);
var chunked = _response.SendChunked;
if (ms != null)
{
var start = ms.Position; // After the possible preamble for the encoding
ms.Position = ms.Length;
if (chunked)
{
bytes = GetChunkSizeBytes(count, false);
ms.Write(bytes, 0, bytes.Length);
}
var newCount = Math.Min(count, 16384 - (int)ms.Position + (int)start);
ms.Write(buffer, offset, newCount);
count -= newCount;
offset += newCount;
InternalWrite(ms.ToArray(), (int)start, (int)(ms.Length - start));
ms.SetLength(0);
ms.Capacity = 0; // 'dispose' the buffer in ms.
}
else if (chunked)
{
bytes = GetChunkSizeBytes(count, false);
InternalWrite(bytes, 0, bytes.Length);
}
if (count > 0)
{
InternalWrite(buffer, offset, count);
}
if (chunked)
{
InternalWrite(CrLf, 0, 2);
}
}
/// <inheritdoc />
public override int Read([In, Out] byte[] buffer, int offset, int count) => throw new NotSupportedException();
/// <inheritdoc />
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc />
public override void SetLength(long value) => throw new NotSupportedException();
internal void InternalWrite(byte[] buffer, int offset, int count)
{
if (_ignoreErrors)
{
try
{
_stream.Write(buffer, offset, count);
}
catch
{
// ignored
}
}
else
{
_stream.Write(buffer, offset, count);
}
}
protected override void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (!disposing)
{
return;
}
using var ms = GetHeaders(true);
var chunked = _response.SendChunked;
if (_stream.CanWrite)
{
try
{
byte[] bytes;
if (ms != null)
{
var start = ms.Position;
if (chunked && !_trailerSent)
{
bytes = GetChunkSizeBytes(0, true);
ms.Position = ms.Length;
ms.Write(bytes, 0, bytes.Length);
}
InternalWrite(ms.ToArray(), (int)start, (int)(ms.Length - start));
_trailerSent = true;
}
else if (chunked && !_trailerSent)
{
bytes = GetChunkSizeBytes(0, true);
InternalWrite(bytes, 0, bytes.Length);
_trailerSent = true;
}
}
catch (ObjectDisposedException)
{
// Ignored
}
catch (IOException)
{
// Ignore error due to connection reset by peer
}
}
_response.Close();
}
private static byte[] GetChunkSizeBytes(int size, bool final) => WebServer.DefaultEncoding.GetBytes($"{size:x}\r\n{(final ? "\r\n" : string.Empty)}");
private MemoryStream? GetHeaders(bool closing)
{
lock (_headersSyncRoot)
{
return _response.HeadersSent ? null : _response.SendHeaders(closing);
}
}
}
}

View File

@@ -0,0 +1,77 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace EmbedIO.Net.Internal
{
internal static class StringExtensions
{
private const string TokenSpecialChars = "()<>@,;:\\\"/[]?={} \t";
internal static bool IsToken(this string @this)
=> @this.All(c => c >= 0x20 && c < 0x7f && TokenSpecialChars.IndexOf(c) < 0);
internal static IEnumerable<string> SplitHeaderValue(this string @this, bool useCookieSeparators)
{
var len = @this.Length;
var buff = new StringBuilder(32);
var escaped = false;
var quoted = false;
for (var i = 0; i < len; i++)
{
var c = @this[i];
if (c == '"')
{
if (escaped)
{
escaped = false;
}
else
{
quoted = !quoted;
}
}
else if (c == '\\')
{
if (i < len - 1 && @this[i + 1] == '"')
{
escaped = true;
}
}
else if (c == ',' || (useCookieSeparators && c == ';'))
{
if (!quoted)
{
yield return buff.ToString();
buff.Length = 0;
continue;
}
}
_ = buff.Append(c);
}
if (buff.Length > 0)
{
yield return buff.ToString();
}
}
internal static string Unquote(this string str)
{
var start = str.IndexOf('\"');
var end = str.LastIndexOf('\"');
if (start >= 0 && end >= 0)
{
str = str.Substring(start + 1, end - 1);
}
return str.Trim();
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
namespace EmbedIO.Net.Internal
{
/// <summary>
/// Represents a wrapper for <c>System.Net.CookieCollection</c>.
/// </summary>
/// <seealso cref="ICookieCollection" />
internal sealed class SystemCookieCollection : ICookieCollection
{
private readonly CookieCollection _collection;
/// <summary>
/// Initializes a new instance of the <see cref="SystemCookieCollection"/> class.
/// </summary>
/// <param name="collection">The cookie collection.</param>
public SystemCookieCollection(CookieCollection collection)
{
_collection = collection;
}
/// <inheritdoc />
public int Count => _collection.Count;
/// <inheritdoc />
public bool IsSynchronized => _collection.IsSynchronized;
/// <inheritdoc />
public object SyncRoot => _collection.SyncRoot;
/// <inheritdoc />
public Cookie? this[string name] => _collection[name];
/// <inheritdoc />
IEnumerator<Cookie> IEnumerable<Cookie>.GetEnumerator() => _collection.OfType<Cookie>().GetEnumerator();
/// <inheritdoc />
public IEnumerator GetEnumerator() => _collection.GetEnumerator();
/// <inheritdoc />
public void CopyTo(Array array, int index) => _collection.CopyTo(array, index);
/// <inheritdoc />
public void CopyTo(Cookie[] array, int index) => _collection.CopyTo(array, index);
/// <inheritdoc />
public void Add(Cookie cookie) => _collection.Add(cookie);
/// <inheritdoc />
public bool Contains(Cookie cookie) => _collection.OfType<Cookie>().Contains(cookie);
}
}

View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using EmbedIO.Authentication;
using EmbedIO.Internal;
using EmbedIO.Routing;
using EmbedIO.Sessions;
using EmbedIO.Utilities;
using EmbedIO.WebSockets;
using EmbedIO.WebSockets.Internal;
using Swan.Logging;
namespace EmbedIO.Net.Internal
{
internal sealed class SystemHttpContext : IHttpContextImpl
{
private readonly System.Net.HttpListenerContext _context;
private readonly TimeKeeper _ageKeeper = new ();
private readonly Stack<Action<IHttpContext>> _closeCallbacks = new ();
private bool _closed;
public SystemHttpContext(System.Net.HttpListenerContext context)
{
_context = context;
Request = new SystemHttpRequest(_context);
User = _context.User ?? Auth.NoUser;
Response = new SystemHttpResponse(_context);
Id = UniqueIdGenerator.GetNext();
LocalEndPoint = Request.LocalEndPoint;
RemoteEndPoint = Request.RemoteEndPoint;
Route = RouteMatch.None;
Session = SessionProxy.None;
}
public string Id { get; }
public CancellationToken CancellationToken { get; set; }
public long Age => _ageKeeper.ElapsedTime;
public IPEndPoint LocalEndPoint { get; }
public IPEndPoint RemoteEndPoint { get; }
public IHttpRequest Request { get; }
public RouteMatch Route { get; set; }
public string RequestedPath => Route.SubPath ?? string.Empty; // It will never be empty, because modules are matched via base routes - this is just to silence a warning.
public IHttpResponse Response { get; }
public IPrincipal User { get; set; }
public ISessionProxy Session { get; set; }
public bool SupportCompressedRequests { get; set; }
public IDictionary<object, object> Items { get; } = new Dictionary<object, object>();
public bool IsHandled { get; private set; }
public MimeTypeProviderStack MimeTypeProviders { get; } = new MimeTypeProviderStack();
public void SetHandled() => IsHandled = true;
public void OnClose(Action<IHttpContext> callback)
{
if (_closed)
{
throw new InvalidOperationException("HTTP context has already been closed.");
}
_closeCallbacks.Push(Validate.NotNull(nameof(callback), callback));
}
public async Task<IWebSocketContext> AcceptWebSocketAsync(
IEnumerable<string> requestedProtocols,
string acceptedProtocol,
int receiveBufferSize,
TimeSpan keepAliveInterval,
CancellationToken cancellationToken)
{
var context = await _context.AcceptWebSocketAsync(
acceptedProtocol.NullIfEmpty(), // Empty string would throw; use null to signify "no subprotocol" here.
receiveBufferSize,
keepAliveInterval)
.ConfigureAwait(false);
return new WebSocketContext(this, context.SecWebSocketVersion, requestedProtocols, acceptedProtocol, new SystemWebSocket(context.WebSocket), cancellationToken);
}
public void Close()
{
_closed = true;
// Always close the response stream no matter what.
Response.Close();
foreach (var callback in _closeCallbacks)
{
try
{
callback(this);
}
catch (Exception e)
{
e.Log("HTTP context", "[Id] Exception thrown by a HTTP context close callback.");
}
}
}
public string GetMimeType(string extension)
=> MimeTypeProviders.GetMimeType(extension);
public bool TryDetermineCompression(string mimeType, out bool preferCompression)
=> MimeTypeProviders.TryDetermineCompression(mimeType, out preferCompression);
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace EmbedIO.Net.Internal
{
/// <summary>
/// Represents a wrapper for Microsoft HTTP Listener.
/// </summary>
internal class SystemHttpListener : IHttpListener
{
private readonly System.Net.HttpListener _httpListener;
public SystemHttpListener(System.Net.HttpListener httpListener)
{
_httpListener = httpListener;
}
/// <inheritdoc />
public bool IgnoreWriteExceptions
{
get => _httpListener.IgnoreWriteExceptions;
set => _httpListener.IgnoreWriteExceptions = value;
}
/// <inheritdoc />
public List<string> Prefixes => _httpListener.Prefixes.ToList();
/// <inheritdoc />
public bool IsListening => _httpListener.IsListening;
/// <inheritdoc />
public string Name { get; } = "Microsoft HTTP Listener";
/// <inheritdoc />
public void Start() => _httpListener.Start();
/// <inheritdoc />
public void Stop() => _httpListener.Stop();
/// <inheritdoc />
public void AddPrefix(string urlPrefix) => _httpListener.Prefixes.Add(urlPrefix);
/// <inheritdoc />
public async Task<IHttpContextImpl> GetContextAsync(CancellationToken cancellationToken)
{
// System.Net.HttpListener.GetContextAsync may throw ObjectDisposedException
// when stopping a WebServer. This has been observed on Mono 5.20.1.19
// on Raspberry Pi, but the fact remains that the method does not take
// a CancellationToken as parameter, and WebServerBase<>.RunAsync counts on it.
System.Net.HttpListenerContext context;
try
{
context = await _httpListener.GetContextAsync().ConfigureAwait(false);
}
catch (Exception e) when (cancellationToken.IsCancellationRequested)
{
throw new OperationCanceledException(
"Probable cancellation detected by catching an exception in System.Net.HttpListener.GetContextAsync",
e,
cancellationToken);
}
return new SystemHttpContext(context);
}
void IDisposable.Dispose() => ((IDisposable)_httpListener)?.Dispose();
}
}

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