diff --git a/src/wan24-AutoDiscover Shared/wan24-AutoDiscover Shared.csproj b/src/wan24-AutoDiscover Shared/wan24-AutoDiscover Shared.csproj index 14243c6..fab8a58 100644 --- a/src/wan24-AutoDiscover Shared/wan24-AutoDiscover Shared.csproj +++ b/src/wan24-AutoDiscover Shared/wan24-AutoDiscover Shared.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/wan24-AutoDiscover/Controllers/DiscoveryController.cs b/src/wan24-AutoDiscover/Controllers/DiscoveryController.cs index 4f24b8c..8187373 100644 --- a/src/wan24-AutoDiscover/Controllers/DiscoveryController.cs +++ b/src/wan24-AutoDiscover/Controllers/DiscoveryController.cs @@ -16,7 +16,7 @@ namespace wan24.AutoDiscover.Controllers /// /// Responses [ApiController, Route("autodiscover")] - public sealed class DiscoveryController(XmlResponseInstances responses) : ControllerBase() + public sealed class DiscoveryController(InstancePool responses) : ControllerBase() { /// /// Max. request length in bytes @@ -40,22 +40,26 @@ public sealed class DiscoveryController(XmlResponseInstances responses) : Contro private const string EMAIL_NODE_NAME = "EMailAddress"; /// - /// Missing email address message + /// Invalid request XML message bytes /// - private static readonly byte[] MissingEmailMessage = "Missing email address in request".GetBytes(); + private static readonly byte[] InvalidRequestMessage = "Invalid Request XML".GetBytes(); /// - /// Invalid email address message + /// Missing email address message bytes /// - private static readonly byte[] InvalidEmailMessage = "Invalid email address in request".GetBytes(); + private static readonly byte[] MissingEmailMessage = "Missing Email Address".GetBytes(); /// - /// Unknown domain name message + /// Invalid email address message bytes /// - private static readonly byte[] UnknownDomainMessage = "Unknown domain name".GetBytes(); + private static readonly byte[] InvalidEmailMessage = "Invalid Email Address".GetBytes(); + /// + /// Unknown domain name message bytes + /// + private static readonly byte[] UnknownDomainMessage = "Unknown Domain Name".GetBytes(); /// /// Responses /// - private readonly XmlResponseInstances Responses = responses; + private readonly InstancePool Responses = responses; /// /// Autodiscover (POX request body required) @@ -72,17 +76,28 @@ public async Task AutoDiscoverAsync() { await HttpContext.Request.Body.CopyToAsync(ms, HttpContext.RequestAborted).DynamicContext(); ms.Position = 0; - using XmlReader xmlRequest = XmlReader.Create(ms); - while (xmlRequest.Read()) + try { - if (xmlRequest.Name != EMAIL_NODE_NAME) continue; - emailAddress = xmlRequest.ReadElementContentAsString(); - break; + using XmlReader requestXml = XmlReader.Create(ms); + while (requestXml.Read()) + { + if (!requestXml.Name.Equals(EMAIL_NODE_NAME, StringComparison.OrdinalIgnoreCase)) + continue; + emailAddress = requestXml.ReadElementContentAsString(); + break; + } + } + catch(XmlException ex) + { + if (Logging.Debug) + Logging.WriteDebug($"Parsing POX request from {HttpContext.Connection.RemoteIpAddress}:{HttpContext.Connection.RemotePort} failed: {ex}"); + RespondBadRequest(InvalidRequestMessage); + return; } } if(emailAddress is null) { - await BadRequestAsync(MissingEmailMessage).DynamicContext(); + RespondBadRequest(MissingEmailMessage); return; } if (Logging.Trace) @@ -90,41 +105,45 @@ public async Task AutoDiscoverAsync() string[] emailParts = emailAddress.Split('@', 2);// @ splitted email alias and domain name if (emailParts.Length != 2 || !MailAddress.TryCreate(emailAddress, out _)) { - await BadRequestAsync(InvalidEmailMessage).DynamicContext(); + RespondBadRequest(InvalidEmailMessage); return; } // Generate the response - using XmlResponse xml = await Responses.GetOneAsync(HttpContext.RequestAborted).DynamicContext();// Response XML if (DomainConfig.GetConfig(HttpContext.Request.Host.Host, emailParts) is not DomainConfig config) { - await BadRequestAsync(UnknownDomainMessage).DynamicContext(); + RespondBadRequest(UnknownDomainMessage); return; } if (Logging.Trace) Logging.WriteTrace($"Creating POX response for \"{emailAddress}\" request from {HttpContext.Connection.RemoteIpAddress}:{HttpContext.Connection.RemotePort}"); HttpContext.Response.StatusCode = OK_STATUS_CODE; HttpContext.Response.ContentType = XML_MIME_TYPE; - await HttpContext.Response.StartAsync(HttpContext.RequestAborted).DynamicContext(); - Task sendXmlOutput = xml.XmlOutput.CopyToAsync(HttpContext.Response.Body, HttpContext.RequestAborted); - config.CreateXml(xml.XML, emailParts); - xml.FinalizeXmlOutput(); - await sendXmlOutput.DynamicContext(); - await HttpContext.Response.CompleteAsync().DynamicContext(); - if (Logging.Trace) - Logging.WriteTrace($"POX response for \"{emailAddress}\" request from {HttpContext.Connection.RemoteIpAddress}:{HttpContext.Connection.RemotePort} sent"); + using XmlResponse responseXml = await Responses.GetOneAsync(HttpContext.RequestAborted).DynamicContext();// Response XML + Task sendXmlOutput = responseXml.XmlOutput.CopyToAsync(HttpContext.Response.Body, HttpContext.RequestAborted); + try + { + config.CreateXml(responseXml.XML, emailParts); + responseXml.FinalizeXmlOutput(); + } + finally + { + await sendXmlOutput.DynamicContext(); + if (Logging.Trace) + Logging.WriteTrace($"POX response for \"{emailAddress}\" request from {HttpContext.Connection.RemoteIpAddress}:{HttpContext.Connection.RemotePort} sent"); + } } /// /// Respond with a bad request message /// /// Message - private async Task BadRequestAsync(ReadOnlyMemory message) + private void RespondBadRequest(byte[] message) { if (Logging.Trace) Logging.WriteTrace($"Invalid POX request from {HttpContext.Connection.RemoteIpAddress}:{HttpContext.Connection.RemotePort}: \"{message.ToUtf8String()}\""); HttpContext.Response.StatusCode = BAD_REQUEST_STATUS_CODE; HttpContext.Response.ContentType = ExceptionHandler.TEXT_MIME_TYPE; - await HttpContext.Response.Body.WriteAsync(message, HttpContext.RequestAborted).DynamicContext(); + HttpContext.Response.Body = new MemoryStream(message); } } } diff --git a/src/wan24-AutoDiscover/Program.cs b/src/wan24-AutoDiscover/Program.cs index f1b942a..bfc3988 100644 --- a/src/wan24-AutoDiscover/Program.cs +++ b/src/wan24-AutoDiscover/Program.cs @@ -45,7 +45,7 @@ async Task LoadConfigAsync() Logging.Logger ??= !string.IsNullOrWhiteSpace(DiscoveryConfig.Current.LogFile) ? await FileLogger.CreateAsync(DiscoveryConfig.Current.LogFile, next: new VividConsoleLogger(), cancellationToken: cts.Token).DynamicContext() : new VividConsoleLogger(); -Logging.WriteInfo($"wan24-AutoDiscover {VersionInfo.Current} Using configuration \"{configFile}\""); +Logging.WriteInfo($"wan24-AutoDiscover {VersionInfo.Current} using configuration \"{configFile}\""); // Watch configuration changes using SemaphoreSync configSync = new(); @@ -128,11 +128,11 @@ async Task LoadConfigAsync() if (ENV.IsLinux) builder.Logging.AddSystemdConsole(); builder.Services.AddControllers(); -builder.Services.AddSingleton(typeof(XmlResponseInstances), services => new XmlResponseInstances(capacity: DiscoveryConfig.Current.PreForkResponses)) +builder.Services.AddExceptionHandler() + .AddSingleton(typeof(InstancePool), services => new InstancePool(capacity: DiscoveryConfig.Current.PreForkResponses)) .AddSingleton(cts) - .AddHostedService(services => services.GetRequiredService()) + .AddHostedService(services => services.GetRequiredService>()) .AddHostedService(services => fsw) - .AddExceptionHandler() .AddHttpLogging(options => options.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders); builder.Services.Configure(options => { @@ -150,7 +150,8 @@ async Task LoadConfigAsync() Logging.WriteInfo("Autodiscovery service app shutdown"); cts.Cancel(); }); - app.MapDefaultEndpoints(); + app.UseExceptionHandler(builder => { });// .NET 8 bugfix :( + app.MapDefaultEndpoints();// Aspire app.UseForwardedHeaders(); if (app.Environment.IsDevelopment()) { @@ -158,8 +159,7 @@ async Task LoadConfigAsync() Logging.WriteTrace("Using development environment"); app.UseHttpLogging(); } - app.UseExceptionHandler(builder => { });// .NET 8 bugfix :( - if (!app.Environment.IsDevelopment()) + else { if (Logging.Trace) Logging.WriteTrace("Using production environment"); diff --git a/src/wan24-AutoDiscover/Services/ExceptionHandler.cs b/src/wan24-AutoDiscover/Services/ExceptionHandler.cs index e72111a..9f6e037 100644 --- a/src/wan24-AutoDiscover/Services/ExceptionHandler.cs +++ b/src/wan24-AutoDiscover/Services/ExceptionHandler.cs @@ -22,14 +22,18 @@ public sealed class ExceptionHandler : IExceptionHandler /// public const string TEXT_MIME_TYPE = "text/plain"; + /// + /// Bad request message bytes + /// + private static readonly byte[] BadRequestMessage = "Bad Request".GetBytes(); /// /// Internal server error message bytes /// - private static readonly byte[] InternalServerErrorMessage = "Internal server error".GetBytes(); + private static readonly byte[] InternalServerErrorMessage = "Internal Server Error".GetBytes(); /// /// Maintenance message bytes /// - private static readonly byte[] MaintenanceMessage = "Temporary not available".GetBytes(); + private static readonly byte[] MaintenanceMessage = "Temporary Not Available".GetBytes(); /// /// Constructor @@ -37,8 +41,9 @@ public sealed class ExceptionHandler : IExceptionHandler public ExceptionHandler() { } /// - public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) + public ValueTask TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) { + if (httpContext.Response.HasStarted) return ValueTask.FromResult(false); CancellationTokenSource cts = httpContext.RequestServices.GetRequiredService(); httpContext.Response.ContentType = TEXT_MIME_TYPE; if (exception is BadHttpRequestException badRequest) @@ -46,7 +51,7 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e if (Logging.Trace) Logging.WriteTrace($"http handling bad request exception for {httpContext.Connection.RemoteIpAddress}:{httpContext.Connection.RemotePort} request to \"{httpContext.Request.Method} {httpContext.Request.Path}\": {exception}"); httpContext.Response.StatusCode = badRequest.StatusCode; - await httpContext.Response.BodyWriter.WriteAsync((badRequest.Message ?? "Bad request").GetBytes(), cancellationToken).DynamicContext(); + httpContext.Response.Body = new MemoryStream(badRequest.Message is null? BadRequestMessage : badRequest.Message.GetBytes()); } else if (exception is OperationCanceledException) { @@ -59,15 +64,15 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e Logging.WriteWarning($"http handling operation canceled exception for {httpContext.Connection.RemoteIpAddress}:{httpContext.Connection.RemotePort} request to \"{httpContext.Request.Method} {httpContext.Request.Path}\": {exception}"); } httpContext.Response.StatusCode = MAINTENANCE_STATUS_CODE; - await httpContext.Response.BodyWriter.WriteAsync(MaintenanceMessage, cancellationToken).DynamicContext(); + httpContext.Response.Body = new MemoryStream(MaintenanceMessage); } else { Logging.WriteError($"http handling exception for {httpContext.Connection.RemoteIpAddress}:{httpContext.Connection.RemotePort} request to \"{httpContext.Request.Method} {httpContext.Request.Path}\": {exception}"); httpContext.Response.StatusCode = INTERNAL_SERVER_ERROR_STATUS_CODE; - await httpContext.Response.BodyWriter.WriteAsync(InternalServerErrorMessage, cancellationToken).DynamicContext(); + httpContext.Response.Body = new MemoryStream(InternalServerErrorMessage); } - return true; + return ValueTask.FromResult(true); } } } diff --git a/src/wan24-AutoDiscover/Services/XmlResponseInstances.cs b/src/wan24-AutoDiscover/Services/XmlResponseInstances.cs deleted file mode 100644 index 1db9a92..0000000 --- a/src/wan24-AutoDiscover/Services/XmlResponseInstances.cs +++ /dev/null @@ -1,11 +0,0 @@ -using wan24.Core; - -namespace wan24.AutoDiscover.Services -{ - /// - /// instances - /// - public sealed class XmlResponseInstances(in int capacity) : InstancePool(capacity) - { - } -} diff --git a/src/wan24-AutoDiscover Shared/XmlResponse.cs b/src/wan24-AutoDiscover/XmlResponse.cs similarity index 74% rename from src/wan24-AutoDiscover Shared/XmlResponse.cs rename to src/wan24-AutoDiscover/XmlResponse.cs index 2ea05be..101b011 100644 --- a/src/wan24-AutoDiscover Shared/XmlResponse.cs +++ b/src/wan24-AutoDiscover/XmlResponse.cs @@ -1,4 +1,5 @@ -using System.Xml; +using System.Text; +using System.Xml; using wan24.Core; namespace wan24.AutoDiscover @@ -8,16 +9,31 @@ namespace wan24.AutoDiscover /// public class XmlResponse : DisposableBase { + /// + /// Buffer size in bytes + /// + private const int BUFFER_SIZE = 1024; + + /// + /// XML writer settings + /// + private static readonly XmlWriterSettings Settings = new() + { + Indent = false, + Encoding = Encoding.UTF8, + OmitXmlDeclaration = true + }; + /// /// Constructor /// public XmlResponse() : base(asyncDisposing: false) { - XmlOutput = new(bufferSize: 1024) + XmlOutput = new(BUFFER_SIZE) { AggressiveReadBlocking = false }; - XML = XmlWriter.Create(XmlOutput); + XML = XmlWriter.Create(XmlOutput, Settings); XML.WriteStartElement(Constants.AUTODISCOVER_NODE_NAME, Constants.AUTO_DISCOVER_NS); XML.WriteStartElement(Constants.RESPONSE_NODE_NAME, Constants.RESPONSE_NS); XML.WriteStartElement(Constants.ACCOUNT_NODE_NAME); diff --git a/src/wan24-AutoDiscover/wan24-AutoDiscover.csproj b/src/wan24-AutoDiscover/wan24-AutoDiscover.csproj index dffd1a7..715b9b3 100644 --- a/src/wan24-AutoDiscover/wan24-AutoDiscover.csproj +++ b/src/wan24-AutoDiscover/wan24-AutoDiscover.csproj @@ -20,8 +20,8 @@ - - + +