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