using System;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using EmbedIO.Internal;
using EmbedIO.Sessions;
using EmbedIO.Utilities;
using Swan.Configuration;
using Swan.Logging;
namespace EmbedIO
{
///
/// Base class for implementations.
///
/// The type of the options object used to configure an instance.
///
///
public abstract class WebServerBase : ConfiguredObject, IWebServer, IHttpContextHandler
where TOptions : WebServerOptionsBase, new()
{
private readonly WebModuleCollection _modules;
private readonly MimeTypeCustomizer _mimeTypeCustomizer = new MimeTypeCustomizer();
private ExceptionHandlerCallback _onUnhandledException = ExceptionHandler.Default;
private HttpExceptionHandlerCallback _onHttpException = HttpExceptionHandler.Default;
private WebServerState _state = WebServerState.Created;
private ISessionManager? _sessionManager;
///
/// Initializes a new instance of the class.
///
protected WebServerBase()
: this(new TOptions(), null)
{
}
///
/// Initializes a new instance of the class.
///
/// A instance that will be used
/// to configure the server.
/// is .
protected WebServerBase(TOptions options)
: this(Validate.NotNull(nameof(options), options), null)
{
}
///
/// Initializes a new instance of the class.
///
/// A callback that will be used to configure
/// the server's options.
/// is .
protected WebServerBase(Action configure)
: this(new TOptions(), Validate.NotNull(nameof(configure), configure))
{
}
private WebServerBase(TOptions options, Action? configure)
{
Options = options;
LogSource = GetType().Name;
_modules = new WebModuleCollection(LogSource);
configure?.Invoke(Options);
Options.Lock();
}
///
/// Finalizes an instance of the class.
///
~WebServerBase()
{
Dispose(false);
}
///
public event WebServerStateChangedEventHandler? StateChanged;
///
public IComponentCollection Modules => _modules;
///
/// Gets the options object used to configure this instance.
///
public TOptions Options { get; }
///
/// The server's configuration is locked.
/// this property is being set to .
///
/// The default value for this property is .
///
///
public ExceptionHandlerCallback OnUnhandledException
{
get => _onUnhandledException;
set
{
EnsureConfigurationNotLocked();
_onUnhandledException = Validate.NotNull(nameof(value), value);
}
}
///
/// The server's configuration is locked.
/// this property is being set to .
///
/// The default value for this property is .
///
///
public HttpExceptionHandlerCallback OnHttpException
{
get => _onHttpException;
set
{
EnsureConfigurationNotLocked();
_onHttpException = Validate.NotNull(nameof(value), value);
}
}
///
public ISessionManager? SessionManager
{
get => _sessionManager;
set
{
EnsureConfigurationNotLocked();
_sessionManager = value;
}
}
///
public WebServerState State
{
get => _state;
private set
{
if (value == _state) return;
var oldState = _state;
_state = value;
if (_state != WebServerState.Created)
{
LockConfiguration();
}
StateChanged?.Invoke(this, new WebServerStateChangedEventArgs(oldState, value));
}
}
///
/// Gets a string to use as a source for log messages.
///
protected string LogSource { get; }
///
public Task HandleContextAsync(IHttpContextImpl context)
{
if (State > WebServerState.Listening)
throw new InvalidOperationException("The web server has already been stopped.");
if (State < WebServerState.Listening)
throw new InvalidOperationException("The web server has not been started yet.");
return DoHandleContextAsync(context);
}
string IMimeTypeProvider.GetMimeType(string extension)
=> _mimeTypeCustomizer.GetMimeType(extension);
bool IMimeTypeProvider.TryDetermineCompression(string mimeType, out bool preferCompression)
=> _mimeTypeCustomizer.TryDetermineCompression(mimeType, out preferCompression);
///
public void AddCustomMimeType(string extension, string mimeType)
=> _mimeTypeCustomizer.AddCustomMimeType(extension, mimeType);
///
public void PreferCompression(string mimeType, bool preferCompression)
=> _mimeTypeCustomizer.PreferCompression(mimeType, preferCompression);
///
/// The method was already called.
/// Cancellation was requested.
public async Task RunAsync(CancellationToken cancellationToken = default)
{
try
{
State = WebServerState.Loading;
Prepare(cancellationToken);
_sessionManager?.Start(cancellationToken);
_modules.StartAll(cancellationToken);
State = WebServerState.Listening;
await ProcessRequestsAsync(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
"Operation canceled.".Debug(LogSource);
}
finally
{
"Cleaning up".Info(LogSource);
State = WebServerState.Stopped;
}
}
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Asynchronously handles a received request.
///
/// The context of the request.
/// A representing the ongoing operation.
protected async Task DoHandleContextAsync(IHttpContextImpl context)
{
context.SupportCompressedRequests = Options.SupportCompressedRequests;
context.MimeTypeProviders.Push(this);
try
{
$"[{context.Id}] {context.Request.SafeGetRemoteEndpointStr()}: {context.Request.HttpMethod} {context.Request.Url.PathAndQuery} - {context.Request.UserAgent}"
.Debug(LogSource);
if (SessionManager != null)
context.Session = new SessionProxy(context, SessionManager);
try
{
if (context.CancellationToken.IsCancellationRequested)
return;
try
{
// Return a 404 (Not Found) response if no module handled the response.
await _modules.DispatchRequestAsync(context).ConfigureAwait(false);
if (!context.IsHandled)
{
$"[{context.Id}] No module generated a response. Sending 404 - Not Found".Error(LogSource);
throw HttpException.NotFound("No module was able to serve the requested path.");
}
}
catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested)
{
throw; // Let outer catch block handle it
}
catch (HttpListenerException)
{
throw; // Let outer catch block handle it
}
catch (Exception exception) when (exception is IHttpException)
{
await HttpExceptionHandler.Handle(LogSource, context, exception, _onHttpException)
.ConfigureAwait(false);
}
catch (Exception exception)
{
await ExceptionHandler.Handle(LogSource, context, exception, _onUnhandledException, _onHttpException)
.ConfigureAwait(false);
}
}
finally
{
await context.Response.OutputStream.FlushAsync(context.CancellationToken)
.ConfigureAwait(false);
var statusCode = context.Response.StatusCode;
var statusDescription = context.Response.StatusDescription;
var sendChunked = context.Response.SendChunked;
var contentLength = context.Response.ContentLength64;
context.Close();
$"[{context.Id}] {context.Request.HttpMethod} {context.Request.Url.AbsolutePath}: \"{statusCode} {statusDescription}\" sent in {context.Age}ms ({(sendChunked ? "chunked" : contentLength.ToString(CultureInfo.InvariantCulture) + " bytes")})"
.Info(LogSource);
}
}
catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested)
{
$"[{context.Id}] Operation canceled.".Debug(LogSource);
}
catch (HttpListenerException ex)
{
ex.Log(LogSource, $"[{context.Id}] Listener exception.");
}
catch (Exception ex)
{
ex.Log(LogSource, $"[{context.Id}] Fatal exception.");
OnFatalException();
}
}
///
protected override void OnBeforeLockConfiguration()
{
base.OnBeforeLockConfiguration();
_mimeTypeCustomizer.Lock();
_modules.Lock();
}
///
/// Releases unmanaged and - optionally - managed resources.
///
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
///
protected virtual void Dispose(bool disposing)
{
if (!disposing)
return;
_modules.Dispose();
}
///
/// Prepares a web server for running.
///
/// A used to stop the web server.
protected virtual void Prepare(CancellationToken cancellationToken)
{
}
///
/// Asynchronously receives requests and processes them.
///
/// A used to stop the web server.
/// A representing the ongoing operation.
protected abstract Task ProcessRequestsAsync(CancellationToken cancellationToken);
///
/// Called when an exception is caught in the web server's request processing loop.
/// This method should tell the server socket to stop accepting further requests.
///
protected abstract void OnFatalException();
}
}