From bc9f9694985ad409916e78f26344980d5f0215e9 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 5 Sep 2022 16:52:16 +0100 Subject: [PATCH 1/9] add cloud event code --- .../AzureEventGridSimulator.csproj | 2 + .../Controllers/NotificationController.cs | 96 ++-- ...ificationCloudEventsToSubscriberCommand.cs | 19 + ...onCloudEventsToSubscriberCommandHandler.cs | 143 ++++++ ...ndNotificationEventsToSubscriberCommand.cs | 9 +- ...icationEventsToSubscriberCommandHandler.cs | 4 +- .../Services/CloudEventValidateService.cs | 55 ++ ...ptionCloudEventSettingsFilterExtensions.cs | 206 ++++++++ .../Middleware/EventGridMiddleware.cs | 394 +++++++++------ src/AzureEventGridSimulator/Program.cs | 478 +++++++++--------- 10 files changed, 976 insertions(+), 430 deletions(-) create mode 100644 src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommand.cs create mode 100644 src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs create mode 100644 src/AzureEventGridSimulator/Domain/Services/CloudEventValidateService.cs create mode 100644 src/AzureEventGridSimulator/Infrastructure/Extensions/SubscriptionCloudEventSettingsFilterExtensions.cs diff --git a/src/AzureEventGridSimulator/AzureEventGridSimulator.csproj b/src/AzureEventGridSimulator/AzureEventGridSimulator.csproj index a5a02dd..61419a6 100644 --- a/src/AzureEventGridSimulator/AzureEventGridSimulator.csproj +++ b/src/AzureEventGridSimulator/AzureEventGridSimulator.csproj @@ -13,6 +13,7 @@ + @@ -23,6 +24,7 @@ + diff --git a/src/AzureEventGridSimulator/Controllers/NotificationController.cs b/src/AzureEventGridSimulator/Controllers/NotificationController.cs index 0e25da3..d7eaefa 100644 --- a/src/AzureEventGridSimulator/Controllers/NotificationController.cs +++ b/src/AzureEventGridSimulator/Controllers/NotificationController.cs @@ -1,39 +1,57 @@ -using System.Linq; -using System.Threading.Tasks; -using AzureEventGridSimulator.Domain; -using AzureEventGridSimulator.Domain.Commands; -using AzureEventGridSimulator.Domain.Entities; -using AzureEventGridSimulator.Infrastructure.Extensions; -using AzureEventGridSimulator.Infrastructure.Settings; -using MediatR; -using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; - -namespace AzureEventGridSimulator.Controllers; - -[Route("/api/events")] -[ApiVersion(Constants.SupportedApiVersion)] -[ApiController] -public class NotificationController : ControllerBase -{ - private readonly IMediator _mediator; - private readonly SimulatorSettings _simulatorSettings; - - public NotificationController(SimulatorSettings simulatorSettings, - IMediator mediator) - { - _mediator = mediator; - _simulatorSettings = simulatorSettings; - } - - [HttpPost] - public async Task Post() - { - var topicSettingsForCurrentRequestPort = _simulatorSettings.Topics.First(t => t.Port == HttpContext.Request.Host.Port); - var eventsFromCurrentRequestBody = JsonConvert.DeserializeObject(await HttpContext.RequestBody()); - - await _mediator.Send(new SendNotificationEventsToSubscriberCommand(eventsFromCurrentRequestBody, topicSettingsForCurrentRequestPort)); - - return Ok(); - } -} +using System; +using System.Linq; +using System.Threading.Tasks; +using Azure.Messaging; +using AzureEventGridSimulator.Domain; +using AzureEventGridSimulator.Domain.Commands; +using AzureEventGridSimulator.Domain.Entities; +using AzureEventGridSimulator.Infrastructure.Extensions; +using AzureEventGridSimulator.Infrastructure.Settings; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace AzureEventGridSimulator.Controllers; + +[Route("/api/events")] +[ApiVersion(Constants.SupportedApiVersion)] +[ApiController] +public class NotificationController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly SimulatorSettings _simulatorSettings; + + public NotificationController(SimulatorSettings simulatorSettings, + IMediator mediator) + { + _mediator = mediator; + _simulatorSettings = simulatorSettings; + } + + [HttpPost] + public async Task Post() + { + var topicSettingsForCurrentRequestPort = _simulatorSettings.Topics.First(t => t.Port == HttpContext.Request.Host.Port); + + var eventsFromCurrentRequestBody = JsonConvert.DeserializeObject(await HttpContext.RequestBody()); + + await _mediator.Send(new SendNotificationEventsToSubscriberCommand(eventsFromCurrentRequestBody, topicSettingsForCurrentRequestPort)); + + return Ok(); + } + + + [Route("cloudevent")] + [HttpPost] + public async Task PostCloudEvent() + { + + var topicSettingsForCurrentRequestPort = _simulatorSettings.Topics.First(t => t.Port == HttpContext.Request.Host.Port); + + var eventsFromCurrentRequestBody = JsonConvert.DeserializeObject(await HttpContext.RequestBody()); + + await _mediator.Send(new SendNotificationCloudEventsToSubscriberCommand(eventsFromCurrentRequestBody, topicSettingsForCurrentRequestPort)); + + return Ok(); + } +} diff --git a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommand.cs b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommand.cs new file mode 100644 index 0000000..0483a64 --- /dev/null +++ b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommand.cs @@ -0,0 +1,19 @@ +using Azure.Messaging; +using AzureEventGridSimulator.Domain.Entities; +using AzureEventGridSimulator.Infrastructure.Settings; +using MediatR; + +namespace AzureEventGridSimulator.Domain.Commands; + +public class SendNotificationEventsToSubscriberCommand : IRequest +{ + public SendNotificationEventsToSubscriberCommand(EventGridEvent[] events, TopicSettings topic) + { + Events = events; + Topic = topic; + } + + public TopicSettings Topic { get; } + + public EventGridEvent[] Events { get; } +} diff --git a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs new file mode 100644 index 0000000..296e8cb --- /dev/null +++ b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Azure.Messaging; +using AzureEventGridSimulator.Domain.Entities; +using AzureEventGridSimulator.Infrastructure.Extensions; +using AzureEventGridSimulator.Infrastructure.Settings; +using MediatR; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace AzureEventGridSimulator.Domain.Commands; + +// ReSharper disable once UnusedMember.Global +public class SendNotificationCloudEventsToSubscriberCommandHandler : AsyncRequestHandler +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + + public SendNotificationCloudEventsToSubscriberCommandHandler(IHttpClientFactory httpClientFactory, ILogger logger) + { + _httpClientFactory = httpClientFactory; + _logger = logger; + } + + protected override Task Handle(SendNotificationCloudEventsToSubscriberCommand request, CancellationToken cancellationToken) + { + _logger.LogInformation("{EventCount} event(s) received on topic '{TopicName}'", request.Events.Length, request.Topic.Name); + + foreach (var eventGridEvent in request.Events) + { + //eventGridEvent.Topic = $"/subscriptions/{Guid.Empty:D}/resourceGroups/eventGridSimulator/providers/Microsoft.EventGrid/topics/{request.Topic.Name}"; + //eventGridEvent.MetadataVersion = "1"; + } + + if (!request.Topic.Subscribers.Any()) + { + _logger.LogWarning("'{TopicName}' has no subscribers so {EventCount} event(s) could not be forwarded", request.Topic.Name, request.Events.Length); + } + else if (request.Topic.Subscribers.All(o => o.Disabled)) + { + _logger.LogWarning("'{TopicName}' has no enabled subscribers so {EventCount} event(s) could not be forwarded", request.Topic.Name, request.Events.Length); + } + else + { + var eventsFilteredOutByAllSubscribers = request.Events + .Where(e => request.Topic.Subscribers.All(s => !s.Filter.AcceptsEvent(e))) + .ToArray(); + + if (eventsFilteredOutByAllSubscribers.Any()) + { + foreach (var eventFilteredOutByAllSubscribers in eventsFilteredOutByAllSubscribers) + { + _logger.LogWarning("All subscribers of topic '{TopicName}' filtered out event {EventId}", + request.Topic.Name, + eventFilteredOutByAllSubscribers.Id); + } + } + else + { + foreach (var subscription in request.Topic.Subscribers) + { +#pragma warning disable 4014 + SendToSubscriber(subscription, request.Events, request.Topic.Name); +#pragma warning restore 4014 + } + } + } + + return Task.CompletedTask; + } + + private async Task SendToSubscriber(SubscriptionSettings subscription, IEnumerable events, string topicName) + { + try + { + if (subscription.Disabled) + { + _logger.LogWarning("Subscription '{SubscriberName}' on topic '{TopicName}' is disabled and so Notification was skipped", subscription.Name, topicName); + return; + } + + if (!subscription.DisableValidation && + subscription.ValidationStatus != SubscriptionValidationStatus.ValidationSuccessful) + { + _logger.LogWarning("Subscription '{SubscriberName}' on topic '{TopicName}' can't receive events. It's still pending validation", subscription.Name, topicName); + return; + } + + _logger.LogDebug("Sending to subscriber '{SubscriberName}' on topic '{TopicName}'", subscription.Name, topicName); + + // "Event Grid sends the events to subscribers in an array that has a single event. This behaviour may change in the future." + // https://docs.microsoft.com/en-us/azure/event-grid/event-schema + foreach (var evt in events) + { + if (subscription.Filter.AcceptsEvent(evt)) + { + var json = JsonConvert.SerializeObject(new[] { evt }, Formatting.Indented); + using var content = new StringContent(json, Encoding.UTF8, "application/json"); + var httpClient = _httpClientFactory.CreateClient(); + httpClient.DefaultRequestHeaders.Add(Constants.AegEventTypeHeader, Constants.NotificationEventType); + httpClient.DefaultRequestHeaders.Add(Constants.AegSubscriptionNameHeader, subscription.Name.ToUpperInvariant()); + //httpClient.DefaultRequestHeaders.Add(Constants.AegDataVersionHeader, evt.DataVersion); + //httpClient.DefaultRequestHeaders.Add(Constants.AegMetadataVersionHeader, evt.MetadataVersion); + httpClient.DefaultRequestHeaders.Add(Constants.AegDeliveryCountHeader, "0"); // TODO implement re-tries + httpClient.Timeout = TimeSpan.FromSeconds(60); + + await httpClient.PostAsync(subscription.Endpoint, content) + .ContinueWith(t => LogResult(t, evt, subscription, topicName)); + } + else + { + _logger.LogDebug("Event {EventId} filtered out for subscriber '{SubscriberName}'", evt.Id, subscription.Name); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send to subscriber '{SubscriberName}'", subscription.Name); + } + } + + private void LogResult(Task task, CloudEvent evt, SubscriptionSettings subscription, string topicName) + { + if (task.IsCompletedSuccessfully && task.Result.IsSuccessStatusCode) + { + _logger.LogDebug("Event {EventId} sent to subscriber '{SubscriberName}' on topic '{TopicName}' successfully", evt.Id, subscription.Name, topicName); + } + else + { + _logger.LogError(task.Exception?.GetBaseException(), + "Failed to send event {EventId} to subscriber '{SubscriberName}', '{TaskStatus}', '{Reason}'", + evt.Id, + subscription.Name, + task.Status.ToString(), + task.Result?.ReasonPhrase); + } + } +} diff --git a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommand.cs b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommand.cs index 607a636..1dcb4ca 100644 --- a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommand.cs +++ b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommand.cs @@ -1,12 +1,13 @@ -using AzureEventGridSimulator.Domain.Entities; +using Azure.Messaging; +using AzureEventGridSimulator.Domain.Entities; using AzureEventGridSimulator.Infrastructure.Settings; using MediatR; namespace AzureEventGridSimulator.Domain.Commands; -public class SendNotificationEventsToSubscriberCommand : IRequest +public class SendNotificationCloudEventsToSubscriberCommand : IRequest { - public SendNotificationEventsToSubscriberCommand(EventGridEvent[] events, TopicSettings topic) + public SendNotificationCloudEventsToSubscriberCommand(CloudEvent[] events, TopicSettings topic) { Events = events; Topic = topic; @@ -14,5 +15,5 @@ public SendNotificationEventsToSubscriberCommand(EventGridEvent[] events, TopicS public TopicSettings Topic { get; } - public EventGridEvent[] Events { get; } + public CloudEvent[] Events { get; } } diff --git a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs index ce09ef8..74eb9c3 100644 --- a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs +++ b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs @@ -97,7 +97,9 @@ private async Task SendToSubscriber(SubscriptionSettings subscription, IEnumerab foreach (var evt in events) { if (subscription.Filter.AcceptsEvent(evt)) - { + { + + // write to azurite instead? var json = JsonConvert.SerializeObject(new[] { evt }, Formatting.Indented); using var content = new StringContent(json, Encoding.UTF8, "application/json"); var httpClient = _httpClientFactory.CreateClient(); diff --git a/src/AzureEventGridSimulator/Domain/Services/CloudEventValidateService.cs b/src/AzureEventGridSimulator/Domain/Services/CloudEventValidateService.cs new file mode 100644 index 0000000..0bf3c44 --- /dev/null +++ b/src/AzureEventGridSimulator/Domain/Services/CloudEventValidateService.cs @@ -0,0 +1,55 @@ +using System; +using Azure.Messaging; +using Microsoft.Extensions.Logging; + +namespace AzureEventGridSimulator.Domain.Services; + +public static class CloudEventValidateService +{ + public static void Validate(CloudEvent @event) + { + if (string.IsNullOrWhiteSpace(@event.Id)) + { + throw new InvalidOperationException($"Required property '{nameof(@event.Id)}' was not set."); + } + + if (string.IsNullOrWhiteSpace(@event.Subject)) + { + throw new InvalidOperationException($"Required property '{nameof(@event.Subject)}' was not set."); + } + + if (string.IsNullOrWhiteSpace(@event.Type)) + { + throw new InvalidOperationException($"Required property '{nameof(@event.Type)}' was not set."); + } + //} + + //if (string.IsNullOrWhiteSpace(EventTime)) + //{ + // throw new InvalidOperationException($"Required property '{nameof(EventTime)}' was not set."); + //} + + //if (!EventTimeIsValid) + //{ + // throw new InvalidOperationException($"The event time property '{nameof(EventTime)}' was not a valid date/time."); + //} + + //if (EventTimeParsed.Kind == DateTimeKind.Unspecified) + //{ + // throw new InvalidOperationException($"Property '{nameof(EventTime)}' must be either Local or UTC."); + //} + + //if (MetadataVersion != null && MetadataVersion != "1") + //{ + // throw new + // InvalidOperationException($"Property '{nameof(MetadataVersion)}' was found to be set to '{MetadataVersion}', but was expected to either be null or be set to 1."); + //} + + //if (!string.IsNullOrEmpty(Topic)) + //{ + // throw new InvalidOperationException($"Property '{nameof(Topic)}' was found to be set to '{Topic}', but was expected to either be null/empty."); + //} + + } + +} diff --git a/src/AzureEventGridSimulator/Infrastructure/Extensions/SubscriptionCloudEventSettingsFilterExtensions.cs b/src/AzureEventGridSimulator/Infrastructure/Extensions/SubscriptionCloudEventSettingsFilterExtensions.cs new file mode 100644 index 0000000..88d91d1 --- /dev/null +++ b/src/AzureEventGridSimulator/Infrastructure/Extensions/SubscriptionCloudEventSettingsFilterExtensions.cs @@ -0,0 +1,206 @@ +using System; +using System.Linq; +using Azure.Messaging; +using AzureEventGridSimulator.Domain.Entities; +using AzureEventGridSimulator.Infrastructure.Settings; +using Newtonsoft.Json.Linq; + +namespace AzureEventGridSimulator.Infrastructure.Extensions; + +public static class SubscriptionCloudEventSettingsFilterExtensions +{ + public static bool AcceptsEvent(this FilterSetting filter, CloudEvent gridEvent) + { + var retVal = filter == null; + + if (retVal) + { + return true; + } + + // we have a filter to parse + retVal = filter.IncludedEventTypes == null + || filter.IncludedEventTypes.Contains("All") + || filter.IncludedEventTypes.Contains(gridEvent.Type); + + // short circuit if we have decided the event type is not acceptable + retVal = retVal + && (string.IsNullOrWhiteSpace(filter.SubjectBeginsWith) + || gridEvent.Subject.StartsWith(filter.SubjectBeginsWith, filter.IsSubjectCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); + + // again, don't bother doing the comparison if we have already decided not to allow the event through the filter + retVal = retVal + && (string.IsNullOrWhiteSpace(filter.SubjectEndsWith) + || gridEvent.Subject.EndsWith(filter.SubjectEndsWith, filter.IsSubjectCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); + + retVal = retVal && (filter.AdvancedFilters ?? Array.Empty()).All(af => af.AcceptsEvent(gridEvent)); + + return retVal; + } + + private static bool AcceptsEvent(this AdvancedFilterSetting filter, CloudEvent gridEvent) + { + var retVal = filter == null; + + if (retVal) + { + return true; + } + + // filter is not null + if (!gridEvent.TryGetValue(filter.Key, out var value)) + { + return false; + } + + switch (filter.OperatorType) + { + case AdvancedFilterSetting.OperatorTypeEnum.NumberGreaterThan: + retVal = Try(() => value.ToNumber() > filter.Value.ToNumber()); + break; + case AdvancedFilterSetting.OperatorTypeEnum.NumberGreaterThanOrEquals: + retVal = Try(() => value.ToNumber() >= filter.Value.ToNumber()); + break; + case AdvancedFilterSetting.OperatorTypeEnum.NumberLessThan: + retVal = Try(() => value.ToNumber() < filter.Value.ToNumber()); + break; + case AdvancedFilterSetting.OperatorTypeEnum.NumberLessThanOrEquals: + retVal = Try(() => value.ToNumber() <= filter.Value.ToNumber()); + break; + case AdvancedFilterSetting.OperatorTypeEnum.NumberIn: + retVal = Try(() => (filter.Values ?? Array.Empty()).Select(v => v.ToNumber()).Contains(value.ToNumber())); + break; + case AdvancedFilterSetting.OperatorTypeEnum.NumberNotIn: + retVal = Try(() => !(filter.Values ?? Array.Empty()).Select(v => v.ToNumber()).Contains(value.ToNumber())); + break; + case AdvancedFilterSetting.OperatorTypeEnum.BoolEquals: + retVal = Try(() => Convert.ToBoolean(value) == Convert.ToBoolean(filter.Value)); + break; + case AdvancedFilterSetting.OperatorTypeEnum.StringContains: + { + // a string cannot be considered to contain null or and empty string + var valueAsString = value as string; + var filterValueAsString = filter.Value as string; + + retVal = Try(() => !string.IsNullOrEmpty(filterValueAsString) && + !string.IsNullOrEmpty(valueAsString) && + valueAsString.Contains(filterValueAsString, StringComparison.OrdinalIgnoreCase)); + } + break; + case AdvancedFilterSetting.OperatorTypeEnum.StringBeginsWith: + { + // null or empty values cannot be considered to be the beginning character of a string + var valueAsString = value as string; + var filterValueAsString = filter.Value as string; + + retVal = Try(() => !string.IsNullOrEmpty(filterValueAsString) && + !string.IsNullOrEmpty(valueAsString) && + valueAsString.StartsWith(filterValueAsString, StringComparison.OrdinalIgnoreCase)); + } + break; + case AdvancedFilterSetting.OperatorTypeEnum.StringEndsWith: + { + // null or empty values cannot be considered to be the end character of a string + var valueAsString = value as string; + var filterValueAsString = filter.Value as string; + + retVal = Try(() => !string.IsNullOrEmpty(filterValueAsString) && + !string.IsNullOrEmpty(valueAsString) && + valueAsString.EndsWith(filterValueAsString, StringComparison.OrdinalIgnoreCase)); + } + break; + case AdvancedFilterSetting.OperatorTypeEnum.StringIn: + retVal = Try(() => (filter.Values ?? Array.Empty()).Select(v => Convert.ToString(v)?.ToUpper()).Contains(Convert.ToString(value)?.ToUpper())); + break; + case AdvancedFilterSetting.OperatorTypeEnum.StringNotIn: + retVal = Try(() => !(filter.Values ?? Array.Empty()).Select(v => Convert.ToString(v)?.ToUpper()).Contains(Convert.ToString(value)?.ToUpper())); + break; + default: + throw new ArgumentOutOfRangeException(nameof(AdvancedFilterSetting.OperatorTypeEnum), "Unknown filter operator"); + } + + return retVal; + } + + private static double ToNumber(this object value) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value), "null is not convertible to a number in this implementation"); + } + + return Convert.ToDouble(value); + } + + private static bool Try(Func function, bool valueOnException = false) + { + try + { + return function(); + } + catch + { + return valueOnException; + } + } + + private static bool TryGetValue(this CloudEvent gridEvent, string key, out object value) + { + var retval = false; + value = null; + + if (!string.IsNullOrWhiteSpace(key)) + { + switch (key) + { + case nameof(gridEvent.Id): + value = gridEvent.Id; + retval = true; + break; + //case nameof(gridEvent.Topic): + // value = gridEvent.Topic; + // retval = true; + // break; + case nameof(gridEvent.Subject): + value = gridEvent.Subject; + retval = true; + break; + case nameof(gridEvent.Type): + value = gridEvent.Type; + retval = true; + break; + //case nameof(gridEvent.DataVersion): + // value = gridEvent.DataVersion; + // retval = true; + // break; + case nameof(gridEvent.Data): + value = gridEvent.Data; + retval = true; + break; + default: + var split = key.Split('.'); + if (split[0] == nameof(gridEvent.Data) && gridEvent.Data != null && split.Length > 1) + { + var tmpValue = gridEvent.Data; + for (var i = 0; i < split.Length; i++) + { + // look for the property on the grid event data object + if (tmpValue != null && JObject.FromObject(tmpValue).TryGetValue(split[i], out var dataValue)) + { + //tmpValue = dataValue.ToObject(); + if (i == split.Length - 1) + { + retval = true; + value = tmpValue; + } + } + } + } + + break; + } + } + + return retval; + } +} diff --git a/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs b/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs index 839d7a0..8f3b030 100644 --- a/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs +++ b/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs @@ -1,147 +1,247 @@ -using System; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using AzureEventGridSimulator.Domain.Entities; -using AzureEventGridSimulator.Infrastructure.Extensions; -using AzureEventGridSimulator.Infrastructure.Settings; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; - -namespace AzureEventGridSimulator.Infrastructure.Middleware; - -public class EventGridMiddleware -{ - private readonly RequestDelegate _next; - - public EventGridMiddleware(RequestDelegate next) - { - _next = next; - } - - // ReSharper disable once UnusedMember.Global - public async Task InvokeAsync(HttpContext context, - SimulatorSettings simulatorSettings, - SasKeyValidator sasHeaderValidator, - ILogger logger) - { - if (IsNotificationRequest(context)) - { - await ValidateNotificationRequest(context, simulatorSettings, sasHeaderValidator, logger); - return; - } - - if (IsValidationRequest(context)) - { - await ValidateSubscriptionValidationRequest(context); - return; - } - - // This is the end of the line. - await context.WriteErrorResponse(HttpStatusCode.BadRequest, "Request not supported.", null); - } - - private async Task ValidateSubscriptionValidationRequest(HttpContext context) - { - var id = context.Request.Query["id"]; - - if (string.IsNullOrWhiteSpace(id)) - { - await context.WriteErrorResponse(HttpStatusCode.BadRequest, "The request did not contain a validation code.", null); - return; - } - - await _next(context); - } - - private async Task ValidateNotificationRequest(HttpContext context, - SimulatorSettings simulatorSettings, - SasKeyValidator sasHeaderValidator, - ILogger logger) - { - var topic = simulatorSettings.Topics.First(t => t.Port == context.Request.Host.Port); - - // - // Validate the key/ token supplied in the header. - // - if (!string.IsNullOrWhiteSpace(topic.Key) && - !sasHeaderValidator.IsValid(context.Request.Headers, topic.Key)) - { - await context.WriteErrorResponse(HttpStatusCode.Unauthorized, "The request did not contain a valid aeg-sas-key or aeg-sas-token.", null); - return; - } - - context.Request.EnableBuffering(); - var requestBody = await context.RequestBody(); - var events = JsonConvert.DeserializeObject(requestBody); - - // - // Validate the overall body size and the size of each event. - // - const int maximumAllowedOverallMessageSizeInBytes = 1536000; - const int maximumAllowedEventGridEventSizeInBytes = 66560; - - if (requestBody.Length > maximumAllowedOverallMessageSizeInBytes) - { - logger.LogError("Payload is larger than the allowed maximum"); - - await context.WriteErrorResponse(HttpStatusCode.RequestEntityTooLarge, "Payload is larger than the allowed maximum.", null); - return; - } - - if (events != null) - { - foreach (var evt in events) - { - var eventSize = JsonConvert.SerializeObject(evt, Formatting.None).Length; - - if (eventSize <= maximumAllowedEventGridEventSizeInBytes) - { - continue; - } - - logger.LogError("Event is larger than the allowed maximum"); - - await context.WriteErrorResponse(HttpStatusCode.RequestEntityTooLarge, "Event is larger than the allowed maximum.", null); - return; - } - - // - // Validate the properties of each event. - // - foreach (var eventGridEvent in events) - { - try - { - eventGridEvent.Validate(); - } - catch (InvalidOperationException ex) - { - logger.LogError(ex, "Event was not valid"); - - await context.WriteErrorResponse(HttpStatusCode.BadRequest, ex.Message, null); - return; - } - } - } - - await _next(context); - } - - private static bool IsNotificationRequest(HttpContext context) - { - return context.Request.Headers.Keys.Any(k => string.Equals(k, "Content-Type", StringComparison.OrdinalIgnoreCase)) && - context.Request.Headers["Content-Type"].Any(v => !string.IsNullOrWhiteSpace(v) && v.Contains("application/json", StringComparison.OrdinalIgnoreCase)) && - context.Request.Method == HttpMethods.Post && - string.Equals(context.Request.Path, "/api/events", StringComparison.OrdinalIgnoreCase); - } - - private static bool IsValidationRequest(HttpContext context) - { - return context.Request.Method == HttpMethods.Get && - string.Equals(context.Request.Path, "/validate", StringComparison.OrdinalIgnoreCase) && - context.Request.Query.Keys.Any(k => string.Equals(k, "id", StringComparison.OrdinalIgnoreCase)) && - Guid.TryParse(context.Request.Query["id"], out _); - } -} +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Azure.Messaging; +using AzureEventGridSimulator.Domain.Entities; +using AzureEventGridSimulator.Domain.Services; +using AzureEventGridSimulator.Infrastructure.Extensions; +using AzureEventGridSimulator.Infrastructure.Settings; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace AzureEventGridSimulator.Infrastructure.Middleware; + +public class EventGridMiddleware +{ + private readonly RequestDelegate _next; + + public EventGridMiddleware(RequestDelegate next) + { + _next = next; + } + + // ReSharper disable once UnusedMember.Global + public async Task InvokeAsync(HttpContext context, + SimulatorSettings simulatorSettings, + SasKeyValidator sasHeaderValidator, + ILogger logger) + { + if (IsNotificationRequest(context)) + { + await ValidateNotificationRequest(context, simulatorSettings, sasHeaderValidator, logger); + return; + } + + if (IsCloudEventNotificationRequest(context)) + { + await ValidateNotificationCloudEventRequest(context, simulatorSettings, sasHeaderValidator, logger); + return; + } + + if (IsValidationRequest(context)) + { + await ValidateSubscriptionValidationRequest(context); + return; + } + + //if (IsCloudEventNotificationRequest(context)) + //{ + // await ValidateSubscriptionValidationRequest(context); + // return; + + //} + + // This is the end of the line. + await context.WriteErrorResponse(HttpStatusCode.BadRequest, "Request not supported.", null); + } + + private async Task ValidateSubscriptionValidationRequest(HttpContext context) + { + var id = context.Request.Query["id"]; + + if (string.IsNullOrWhiteSpace(id)) + { + await context.WriteErrorResponse(HttpStatusCode.BadRequest, "The request did not contain a validation code.", null); + return; + } + + await _next(context); + } + + private async Task ValidateNotificationRequest(HttpContext context, + SimulatorSettings simulatorSettings, + SasKeyValidator sasHeaderValidator, + ILogger logger) + { + var topic = simulatorSettings.Topics.First(t => t.Port == context.Request.Host.Port); + + // + // Validate the key/ token supplied in the header. + // + if (!string.IsNullOrWhiteSpace(topic.Key) && + !sasHeaderValidator.IsValid(context.Request.Headers, topic.Key)) + { + await context.WriteErrorResponse(HttpStatusCode.Unauthorized, "The request did not contain a valid aeg-sas-key or aeg-sas-token.", null); + return; + } + + context.Request.EnableBuffering(); + var requestBody = await context.RequestBody(); + var events = JsonConvert.DeserializeObject(requestBody); + + // + // Validate the overall body size and the size of each event. + // + const int maximumAllowedOverallMessageSizeInBytes = 1536000; + const int maximumAllowedEventGridEventSizeInBytes = 66560; + + if (requestBody.Length > maximumAllowedOverallMessageSizeInBytes) + { + logger.LogError("Payload is larger than the allowed maximum"); + + await context.WriteErrorResponse(HttpStatusCode.RequestEntityTooLarge, "Payload is larger than the allowed maximum.", null); + return; + } + + if (events != null) + { + foreach (var evt in events) + { + var eventSize = JsonConvert.SerializeObject(evt, Formatting.None).Length; + + if (eventSize <= maximumAllowedEventGridEventSizeInBytes) + { + continue; + } + + logger.LogError("Event is larger than the allowed maximum"); + + await context.WriteErrorResponse(HttpStatusCode.RequestEntityTooLarge, "Event is larger than the allowed maximum.", null); + return; + } + + // + // Validate the properties of each event. + // + foreach (var eventGridEvent in events) + { + try + { + eventGridEvent.Validate(); + } + catch (InvalidOperationException ex) + { + logger.LogError(ex, "Event was not valid"); + + await context.WriteErrorResponse(HttpStatusCode.BadRequest, ex.Message, null); + return; + } + } + } + + await _next(context); + } + + + private async Task ValidateNotificationCloudEventRequest(HttpContext context, + SimulatorSettings simulatorSettings, + SasKeyValidator sasHeaderValidator, + ILogger logger) + { + var topic = simulatorSettings.Topics.First(t => t.Port == context.Request.Host.Port); + + // + // Validate the key/ token supplied in the header. + // + if (!string.IsNullOrWhiteSpace(topic.Key) && + !sasHeaderValidator.IsValid(context.Request.Headers, topic.Key)) + { + await context.WriteErrorResponse(HttpStatusCode.Unauthorized, "The request did not contain a valid aeg-sas-key or aeg-sas-token.", null); + return; + } + + context.Request.EnableBuffering(); + var requestBody = await context.RequestBody(); + var events = JsonConvert.DeserializeObject(requestBody); + + // + // Validate the overall body size and the size of each event. + // + const int maximumAllowedOverallMessageSizeInBytes = 1536000; + const int maximumAllowedEventGridEventSizeInBytes = 66560; + + if (requestBody.Length > maximumAllowedOverallMessageSizeInBytes) + { + logger.LogError("Payload is larger than the allowed maximum"); + + await context.WriteErrorResponse(HttpStatusCode.RequestEntityTooLarge, "Payload is larger than the allowed maximum.", null); + return; + } + + if (events != null) + { + foreach (var evt in events) + { + var eventSize = JsonConvert.SerializeObject(evt, Formatting.None).Length; + + if (eventSize <= maximumAllowedEventGridEventSizeInBytes) + { + continue; + } + + logger.LogError("Event is larger than the allowed maximum"); + + await context.WriteErrorResponse(HttpStatusCode.RequestEntityTooLarge, "Event is larger than the allowed maximum.", null); + return; + } + + // + // Validate the properties of each event. + // + foreach (var eventGridEvent in events) + { + try + { + CloudEventValidateService.Validate(eventGridEvent); + + } + catch (InvalidOperationException ex) + { + logger.LogError(ex, "Event was not valid"); + + await context.WriteErrorResponse(HttpStatusCode.BadRequest, ex.Message, null); + return; + } + } + } + + await _next(context); + } + + + private static bool IsNotificationRequest(HttpContext context) + { + return context.Request.Headers.Keys.Any(k => string.Equals(k, "Content-Type", StringComparison.OrdinalIgnoreCase)) && + context.Request.Headers["Content-Type"].Any(v => !string.IsNullOrWhiteSpace(v) && v.Contains("application/json", StringComparison.OrdinalIgnoreCase)) && + context.Request.Method == HttpMethods.Post && + string.Equals(context.Request.Path, "/api/events", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsCloudEventNotificationRequest(HttpContext context) + { + return context.Request.Headers.Keys.Any(k => string.Equals(k, "Content-Type", StringComparison.OrdinalIgnoreCase)) && + context.Request.Headers["Content-Type"].Any(v => !string.IsNullOrWhiteSpace(v) && v.Contains("application/json", StringComparison.OrdinalIgnoreCase)) && + context.Request.Method == HttpMethods.Post && + string.Equals(context.Request.Path, "/api/events/cloudevent", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsValidationRequest(HttpContext context) + { + return context.Request.Method == HttpMethods.Get && + string.Equals(context.Request.Path, "/validate", StringComparison.OrdinalIgnoreCase) && + context.Request.Query.Keys.Any(k => string.Equals(k, "id", StringComparison.OrdinalIgnoreCase)) && + Guid.TryParse(context.Request.Query["id"], out _); + } +} diff --git a/src/AzureEventGridSimulator/Program.cs b/src/AzureEventGridSimulator/Program.cs index e52f476..76e484b 100644 --- a/src/AzureEventGridSimulator/Program.cs +++ b/src/AzureEventGridSimulator/Program.cs @@ -1,239 +1,239 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using AzureEventGridSimulator.Domain; -using AzureEventGridSimulator.Domain.Commands; -using AzureEventGridSimulator.Infrastructure; -using AzureEventGridSimulator.Infrastructure.Extensions; -using AzureEventGridSimulator.Infrastructure.Middleware; -using AzureEventGridSimulator.Infrastructure.Settings; -using MediatR; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Versioning; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Serilog; -using Serilog.Events; -using ILogger = Serilog.ILogger; - -[assembly: InternalsVisibleTo("AzureEventGridSimulator.Tests")] - -namespace AzureEventGridSimulator; - -public class Program -{ - public static async Task Main(string[] args) - { - try - { - // Build it and fire it up - var app = CreateWebHostBuilder(args) - .Build(); - - app.UseSerilogRequestLogging(options => { options.GetLevel = (_, _, _) => LogEventLevel.Debug; }); - app.UseEventGridMiddleware(); - app.UseRouting(); - app.UseEndpoints(e => { e.MapControllers(); }); - - await StartSimulator(app); - } - catch (Exception ex) - { - Log.Fatal(ex, "Failed to start the Azure Event Grid Simulator"); - } - finally - { - Log.CloseAndFlush(); - } - } - - public static async Task StartSimulator(WebApplication host, CancellationToken token = default) - { - try - { - await host.StartAsync(token) - .ContinueWith(_ => OnApplicationStarted(host, host.Lifetime), token) - .ConfigureAwait(false); - - await host.WaitForShutdownAsync(token).ConfigureAwait(false); - } - finally - { - await host.DisposeAsync().ConfigureAwait(false); - } - } - - private static async Task OnApplicationStarted(IApplicationBuilder app, IHostApplicationLifetime lifetime) - { - try - { - Log.Verbose("Started"); - - var simulatorSettings = app.ApplicationServices.GetService(); - - if (simulatorSettings is null) - { - Log.Fatal("Settings are not found. The application will now exit"); - lifetime.StopApplication(); - return; - } - - if (!simulatorSettings.Topics.Any()) - { - Log.Fatal("There are no configured topics. The application will now exit"); - lifetime.StopApplication(); - return; - } - - if (simulatorSettings.Topics.All(o => o.Disabled)) - { - Log.Fatal("All of the configured topics are disabled. The application will now exit"); - lifetime.StopApplication(); - return; - } - - var mediator = app.ApplicationServices.GetService(); - - if (mediator is null) - { - Log.Fatal("Required component was not found. The application will now exit"); - lifetime.StopApplication(); - return; - } - - await mediator.Send(new ValidateAllSubscriptionsCommand()); - - Log.Information("It's alive !"); - } - catch (Exception e) - { - Log.Fatal(e, "It died !"); - lifetime.StopApplication(); - } - } - - private static WebApplicationBuilder CreateWebHostBuilder(string[] args) - { - // Set up basic Console logger we can use to log to until we've finished building everything - Log.Logger = CreateBasicConsoleLogger(); - - // First thing's first. Build the configuration. - var configuration = BuildConfiguration(args); - - // Configure the web host builder - return ConfigureWebHost(args, configuration); - } - - private static ILogger CreateBasicConsoleLogger() - { - return new LoggerConfiguration() - .MinimumLevel.Is(LogEventLevel.Verbose) - .MinimumLevel.Override("Microsoft", LogEventLevel.Error) - .MinimumLevel.Override("System", LogEventLevel.Error) - .WriteTo.Console() - .CreateBootstrapLogger(); - } - - private static IConfigurationRoot BuildConfiguration(string[] args) - { - var environmentAndCommandLineConfiguration = new ConfigurationBuilder() - .AddEnvironmentVariablesAndCommandLine(args) - .Build(); - - var environmentName = environmentAndCommandLineConfiguration.EnvironmentName(); - - var builder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", true, false) - .AddJsonFile($"appsettings.{environmentName}.json", true, false) - .AddCustomSimulatorConfigFileIfSpecified(environmentAndCommandLineConfiguration) - .AddEnvironmentVariablesAndCommandLine(args) - .AddInMemoryCollection( - new Dictionary - { - ["AEGS_Serilog__Using__0"] = "Serilog.Sinks.Console", - ["AEGS_Serilog__Using__1"] = "Serilog.Sinks.File", - ["AEGS_Serilog__Using__2"] = "Serilog.Sinks.Seq" - }); - - return builder.Build(); - } - - private static WebApplicationBuilder ConfigureWebHost(string[] args, IConfiguration configuration) - { - var builder = WebApplication.CreateBuilder(args); - - builder.Host.ConfigureServices(services => - { - services.AddSimulatorSettings(configuration); - services.AddMediatR(Assembly.GetExecutingAssembly()); - services.AddHttpClient(); - - services.AddScoped(); - services.AddSingleton(); - - services.AddControllers(options => { options.EnableEndpointRouting = false; }) - .AddJsonOptions(options => { options.JsonSerializerOptions.WriteIndented = true; }); - - services.AddApiVersioning(config => - { - config.DefaultApiVersion = new ApiVersion(DateTime.Parse(Constants.SupportedApiVersion, new ApiVersionFormatProvider())); - config.AssumeDefaultVersionWhenUnspecified = true; - config.ReportApiVersions = true; - }); - }); - - builder.Host.ConfigureLogging(loggingBuilder => { loggingBuilder.ClearProviders(); }); - builder.Host.UseSerilog((context, loggerConfiguration) => - { - var hasAtLeastOneLogSinkBeenConfigured = context.Configuration.GetSection("Serilog:WriteTo").GetChildren().ToArray().Any(); - - loggerConfiguration - .Enrich.FromLogContext() - .Enrich.WithProperty("MachineName", Environment.MachineName) - .Enrich.WithProperty("Environment", context.Configuration.EnvironmentName()) - .Enrich.WithProperty("Application", nameof(AzureEventGridSimulator)) - .Enrich.WithProperty("Version", Assembly.GetExecutingAssembly().GetName().Version) - // The sensible defaults - .MinimumLevel.Is(LogEventLevel.Information) - .MinimumLevel.Override("Microsoft", LogEventLevel.Error) - .MinimumLevel.Override("System", LogEventLevel.Error) - // Override defaults from settings if any - .ReadFrom.Configuration(context.Configuration, "Serilog") - .WriteTo.Conditional(_ => !hasAtLeastOneLogSinkBeenConfigured, sinkConfiguration => sinkConfiguration.Console()); - }); - - builder.WebHost - .ConfigureAppConfiguration((_, configBuilder) => - { - //configBuilder.Sources.Clear(); - configBuilder.AddConfiguration(configuration); - }) - .UseKestrel(options => - { - Log.Verbose(((IConfigurationRoot)configuration).GetDebugView().Normalize()); - - options.ConfigureSimulatorCertificate(); - - foreach (var topics in options.ApplicationServices.EnabledTopics()) - { - options.Listen(IPAddress.Any, - topics.Port, - listenOptions => listenOptions - .UseHttps()); - } - }); - - return builder; - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using AzureEventGridSimulator.Domain; +using AzureEventGridSimulator.Domain.Commands; +using AzureEventGridSimulator.Infrastructure; +using AzureEventGridSimulator.Infrastructure.Extensions; +using AzureEventGridSimulator.Infrastructure.Middleware; +using AzureEventGridSimulator.Infrastructure.Settings; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Versioning; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Events; +using ILogger = Serilog.ILogger; + +[assembly: InternalsVisibleTo("AzureEventGridSimulator.Tests")] + +namespace AzureEventGridSimulator; + +public class Program +{ + public static async Task Main(string[] args) + { + try + { + // Build it and fire it up + var app = CreateWebHostBuilder(args) + .Build(); + + app.UseSerilogRequestLogging(options => { options.GetLevel = (_, _, _) => LogEventLevel.Debug; }); + app.UseEventGridMiddleware(); + app.UseRouting(); + app.UseEndpoints(e => { e.MapControllers(); }); + + await StartSimulator(app); + } + catch (Exception ex) + { + Log.Fatal(ex, "Failed to start the Azure Event Grid Simulator"); + } + finally + { + Log.CloseAndFlush(); + } + } + + public static async Task StartSimulator(WebApplication host, CancellationToken token = default) + { + try + { + await host.StartAsync(token) + .ContinueWith(_ => OnApplicationStarted(host, host.Lifetime), token) + .ConfigureAwait(false); + + await host.WaitForShutdownAsync(token).ConfigureAwait(false); + } + finally + { + await host.DisposeAsync().ConfigureAwait(false); + } + } + + private static async Task OnApplicationStarted(IApplicationBuilder app, IHostApplicationLifetime lifetime) + { + try + { + Log.Verbose("Started"); + + var simulatorSettings = app.ApplicationServices.GetService(); + + if (simulatorSettings is null) + { + Log.Fatal("Settings are not found. The application will now exit"); + lifetime.StopApplication(); + return; + } + + if (!simulatorSettings.Topics.Any()) + { + Log.Fatal("There are no configured topics. The application will now exit"); + lifetime.StopApplication(); + return; + } + + if (simulatorSettings.Topics.All(o => o.Disabled)) + { + Log.Fatal("All of the configured topics are disabled. The application will now exit"); + lifetime.StopApplication(); + return; + } + + var mediator = app.ApplicationServices.GetService(); + + if (mediator is null) + { + Log.Fatal("Required component was not found. The application will now exit"); + lifetime.StopApplication(); + return; + } + + await mediator.Send(new ValidateAllSubscriptionsCommand()); + + Log.Information("It's alive !"); + } + catch (Exception e) + { + Log.Fatal(e, "It died !"); + lifetime.StopApplication(); + } + } + + private static WebApplicationBuilder CreateWebHostBuilder(string[] args) + { + // Set up basic Console logger we can use to log to until we've finished building everything + Log.Logger = CreateBasicConsoleLogger(); + + // First thing's first. Build the configuration. + var configuration = BuildConfiguration(args); + + // Configure the web host builder + return ConfigureWebHost(args, configuration); + } + + private static ILogger CreateBasicConsoleLogger() + { + return new LoggerConfiguration() + .MinimumLevel.Is(LogEventLevel.Verbose) + .MinimumLevel.Override("Microsoft", LogEventLevel.Error) + .MinimumLevel.Override("System", LogEventLevel.Error) + .WriteTo.Console() + .CreateBootstrapLogger(); + } + + private static IConfigurationRoot BuildConfiguration(string[] args) + { + var environmentAndCommandLineConfiguration = new ConfigurationBuilder() + .AddEnvironmentVariablesAndCommandLine(args) + .Build(); + + var environmentName = environmentAndCommandLineConfiguration.EnvironmentName(); + + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{environmentName}.json", true, false) + .AddCustomSimulatorConfigFileIfSpecified(environmentAndCommandLineConfiguration) + .AddEnvironmentVariablesAndCommandLine(args) + .AddInMemoryCollection( + new Dictionary + { + ["AEGS_Serilog__Using__0"] = "Serilog.Sinks.Console", + ["AEGS_Serilog__Using__1"] = "Serilog.Sinks.File", + ["AEGS_Serilog__Using__2"] = "Serilog.Sinks.Seq" + }); + + return builder.Build(); + } + + private static WebApplicationBuilder ConfigureWebHost(string[] args, IConfiguration configuration) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Host.ConfigureServices(services => + { + services.AddSimulatorSettings(configuration); + services.AddMediatR(Assembly.GetExecutingAssembly()); + services.AddHttpClient(); + + services.AddScoped(); + services.AddSingleton(); + + services.AddControllers(options => { options.EnableEndpointRouting = false; }) + .AddJsonOptions(options => { options.JsonSerializerOptions.WriteIndented = true; }); + + services.AddApiVersioning(config => + { + config.DefaultApiVersion = new ApiVersion(DateTime.Parse(Constants.SupportedApiVersion, new ApiVersionFormatProvider())); + config.AssumeDefaultVersionWhenUnspecified = true; + config.ReportApiVersions = true; + }); + }); + + builder.Host.ConfigureLogging(loggingBuilder => { loggingBuilder.ClearProviders(); }); + builder.Host.UseSerilog((context, loggerConfiguration) => + { + var hasAtLeastOneLogSinkBeenConfigured = context.Configuration.GetSection("Serilog:WriteTo").GetChildren().ToArray().Any(); + + loggerConfiguration + .Enrich.FromLogContext() + .Enrich.WithProperty("MachineName", Environment.MachineName) + .Enrich.WithProperty("Environment", context.Configuration.EnvironmentName()) + .Enrich.WithProperty("Application", nameof(AzureEventGridSimulator)) + .Enrich.WithProperty("Version", Assembly.GetExecutingAssembly().GetName().Version) + // The sensible defaults + .MinimumLevel.Is(LogEventLevel.Information) + .MinimumLevel.Override("Microsoft", LogEventLevel.Error) + .MinimumLevel.Override("System", LogEventLevel.Error) + // Override defaults from settings if any + .ReadFrom.Configuration(context.Configuration, "Serilog") + .WriteTo.Conditional(_ => !hasAtLeastOneLogSinkBeenConfigured, sinkConfiguration => sinkConfiguration.Console()); + }); + + builder.WebHost + .ConfigureAppConfiguration((_, configBuilder) => + { + //configBuilder.Sources.Clear(); + configBuilder.AddConfiguration(configuration); + }) + .UseKestrel(options => + { + Log.Verbose(((IConfigurationRoot)configuration).GetDebugView().Normalize()); + + options.ConfigureSimulatorCertificate(); + + foreach (var topics in options.ApplicationServices.EnabledTopics()) + { + options.Listen(IPAddress.Any, + topics.Port, + listenOptions => listenOptions + .UseHttps()); + } + }); + + return builder; + } +} From d5b76012ec12397741bd1d9a0053f1a87d13f0d5 Mon Sep 17 00:00:00 2001 From: DanielBarrettHE Date: Wed, 7 Sep 2022 16:13:56 +0100 Subject: [PATCH 2/9] CloudEvent wrapper --- .../Controllers/NotificationController.cs | 2 +- ...onCloudEventsToSubscriberCommandHandler.cs | 4 +-- ...ndNotificationEventsToSubscriberCommand.cs | 4 +-- .../Domain/Entities/CloudEventGridEvent.cs | 35 +++++++++++++++++++ .../Services/CloudEventValidateService.cs | 3 +- ...ptionCloudEventSettingsFilterExtensions.cs | 14 ++++---- .../Middleware/EventGridMiddleware.cs | 12 +++++-- 7 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs diff --git a/src/AzureEventGridSimulator/Controllers/NotificationController.cs b/src/AzureEventGridSimulator/Controllers/NotificationController.cs index d7eaefa..177001e 100644 --- a/src/AzureEventGridSimulator/Controllers/NotificationController.cs +++ b/src/AzureEventGridSimulator/Controllers/NotificationController.cs @@ -48,7 +48,7 @@ public async Task PostCloudEvent() var topicSettingsForCurrentRequestPort = _simulatorSettings.Topics.First(t => t.Port == HttpContext.Request.Host.Port); - var eventsFromCurrentRequestBody = JsonConvert.DeserializeObject(await HttpContext.RequestBody()); + var eventsFromCurrentRequestBody = JsonConvert.DeserializeObject(await HttpContext.RequestBody()); await _mediator.Send(new SendNotificationCloudEventsToSubscriberCommand(eventsFromCurrentRequestBody, topicSettingsForCurrentRequestPort)); diff --git a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs index 296e8cb..3a65d2f 100644 --- a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs +++ b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs @@ -74,7 +74,7 @@ protected override Task Handle(SendNotificationCloudEventsToSubscriberCommand re return Task.CompletedTask; } - private async Task SendToSubscriber(SubscriptionSettings subscription, IEnumerable events, string topicName) + private async Task SendToSubscriber(SubscriptionSettings subscription, IEnumerable events, string topicName) { try { @@ -124,7 +124,7 @@ await httpClient.PostAsync(subscription.Endpoint, content) } } - private void LogResult(Task task, CloudEvent evt, SubscriptionSettings subscription, string topicName) + private void LogResult(Task task, CloudEventGridEvent evt, SubscriptionSettings subscription, string topicName) { if (task.IsCompletedSuccessfully && task.Result.IsSuccessStatusCode) { diff --git a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommand.cs b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommand.cs index 1dcb4ca..d84ee7f 100644 --- a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommand.cs +++ b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommand.cs @@ -7,7 +7,7 @@ namespace AzureEventGridSimulator.Domain.Commands; public class SendNotificationCloudEventsToSubscriberCommand : IRequest { - public SendNotificationCloudEventsToSubscriberCommand(CloudEvent[] events, TopicSettings topic) + public SendNotificationCloudEventsToSubscriberCommand(CloudEventGridEvent[] events, TopicSettings topic) { Events = events; Topic = topic; @@ -15,5 +15,5 @@ public SendNotificationCloudEventsToSubscriberCommand(CloudEvent[] events, Topic public TopicSettings Topic { get; } - public CloudEvent[] Events { get; } + public CloudEventGridEvent[] Events { get; } } diff --git a/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs b/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs new file mode 100644 index 0000000..18e4011 --- /dev/null +++ b/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs @@ -0,0 +1,35 @@ +using System; +using System.Runtime.Serialization; +using Newtonsoft.Json; + +namespace AzureEventGridSimulator.Domain.Entities; +public class CloudEventGridEvent +{ + [DataMember(Name = "id")] + [JsonProperty("id")] + public string Id { get; set; } + [DataMember(Name = "source")] + [JsonProperty("source")] + public string Source { get; set; } + [DataMember(Name = "type")] + [JsonProperty("type")] + public string Type { get; set; } + [DataMember(Name = "data_base64")] + [JsonProperty("data_base64")] + public string Data_Base64 { get; set; } + [DataMember(Name = "time")] + [JsonProperty("time")] + public DateTimeOffset Time { get; set; } + [DataMember(Name = "specversion")] + [JsonProperty("specversion")] + public string SpecVersion { get; set; } + [DataMember(Name = "dataschema")] + [JsonProperty("dataschema")] + public string DataSchema { get; set; } + [DataMember(Name = "datacontenttype")] + [JsonProperty("datacontenttype")] + public string DataContentType { get; set; } + [DataMember(Name = "subject")] + [JsonProperty("subject")] + public string Subject { get; set; } +} diff --git a/src/AzureEventGridSimulator/Domain/Services/CloudEventValidateService.cs b/src/AzureEventGridSimulator/Domain/Services/CloudEventValidateService.cs index 0bf3c44..ce43281 100644 --- a/src/AzureEventGridSimulator/Domain/Services/CloudEventValidateService.cs +++ b/src/AzureEventGridSimulator/Domain/Services/CloudEventValidateService.cs @@ -1,12 +1,13 @@ using System; using Azure.Messaging; +using AzureEventGridSimulator.Domain.Entities; using Microsoft.Extensions.Logging; namespace AzureEventGridSimulator.Domain.Services; public static class CloudEventValidateService { - public static void Validate(CloudEvent @event) + public static void Validate(CloudEventGridEvent @event) { if (string.IsNullOrWhiteSpace(@event.Id)) { diff --git a/src/AzureEventGridSimulator/Infrastructure/Extensions/SubscriptionCloudEventSettingsFilterExtensions.cs b/src/AzureEventGridSimulator/Infrastructure/Extensions/SubscriptionCloudEventSettingsFilterExtensions.cs index 88d91d1..8ca0ce6 100644 --- a/src/AzureEventGridSimulator/Infrastructure/Extensions/SubscriptionCloudEventSettingsFilterExtensions.cs +++ b/src/AzureEventGridSimulator/Infrastructure/Extensions/SubscriptionCloudEventSettingsFilterExtensions.cs @@ -9,7 +9,7 @@ namespace AzureEventGridSimulator.Infrastructure.Extensions; public static class SubscriptionCloudEventSettingsFilterExtensions { - public static bool AcceptsEvent(this FilterSetting filter, CloudEvent gridEvent) + public static bool AcceptsEvent(this FilterSetting filter, CloudEventGridEvent gridEvent) { var retVal = filter == null; @@ -38,7 +38,7 @@ public static bool AcceptsEvent(this FilterSetting filter, CloudEvent gridEvent) return retVal; } - private static bool AcceptsEvent(this AdvancedFilterSetting filter, CloudEvent gridEvent) + private static bool AcceptsEvent(this AdvancedFilterSetting filter, CloudEventGridEvent gridEvent) { var retVal = filter == null; @@ -144,7 +144,7 @@ private static bool Try(Func function, bool valueOnException = false) } } - private static bool TryGetValue(this CloudEvent gridEvent, string key, out object value) + private static bool TryGetValue(this CloudEventGridEvent gridEvent, string key, out object value) { var retval = false; value = null; @@ -173,15 +173,15 @@ private static bool TryGetValue(this CloudEvent gridEvent, string key, out objec // value = gridEvent.DataVersion; // retval = true; // break; - case nameof(gridEvent.Data): - value = gridEvent.Data; + case nameof(gridEvent.Data_Base64): + value = gridEvent.Data_Base64; retval = true; break; default: var split = key.Split('.'); - if (split[0] == nameof(gridEvent.Data) && gridEvent.Data != null && split.Length > 1) + if (split[0] == nameof(gridEvent.Data_Base64) && gridEvent.Data_Base64 != null && split.Length > 1) { - var tmpValue = gridEvent.Data; + var tmpValue = gridEvent.Data_Base64; for (var i = 0; i < split.Length; i++) { // look for the property on the grid event data object diff --git a/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs b/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs index 8f3b030..2fcea13 100644 --- a/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs +++ b/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Linq; using System.Net; using System.Threading.Tasks; @@ -28,6 +29,13 @@ public async Task InvokeAsync(HttpContext context, SasKeyValidator sasHeaderValidator, ILogger logger) { + logger.LogInformation("Headers:"); + logger.LogInformation(JsonConvert.SerializeObject(context.Request.Headers)); + logger.LogInformation("Path:"); + logger.LogInformation(context.Request.Path); + logger.LogInformation("Query string: "); + logger.LogInformation(JsonConvert.SerializeObject(context.Request.QueryString)); + if (IsNotificationRequest(context)) { await ValidateNotificationRequest(context, simulatorSettings, sasHeaderValidator, logger); @@ -164,7 +172,7 @@ private async Task ValidateNotificationCloudEventRequest(HttpContext context, context.Request.EnableBuffering(); var requestBody = await context.RequestBody(); - var events = JsonConvert.DeserializeObject(requestBody); + var events = JsonConvert.DeserializeObject(requestBody); // // Validate the overall body size and the size of each event. @@ -232,7 +240,7 @@ private static bool IsNotificationRequest(HttpContext context) private static bool IsCloudEventNotificationRequest(HttpContext context) { return context.Request.Headers.Keys.Any(k => string.Equals(k, "Content-Type", StringComparison.OrdinalIgnoreCase)) && - context.Request.Headers["Content-Type"].Any(v => !string.IsNullOrWhiteSpace(v) && v.Contains("application/json", StringComparison.OrdinalIgnoreCase)) && + context.Request.Headers["Content-Type"].Any(v => !string.IsNullOrWhiteSpace(v) && (v.Contains("application/json", StringComparison.OrdinalIgnoreCase) || v.Contains("application/cloudevents-batch+json", StringComparison.OrdinalIgnoreCase))) && context.Request.Method == HttpMethods.Post && string.Equals(context.Request.Path, "/api/events/cloudevent", StringComparison.OrdinalIgnoreCase); } From ecb95c2eabb10f851225b0e6d21f14473e5d0863 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 8 Sep 2022 11:56:33 +0100 Subject: [PATCH 3/9] update readme to include cloud event --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 83db7f3..1a2efbf 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![GitHub all releases](https://img.shields.io/github/downloads/pmcilreavy/AzureEventGridSimulator/total) ![Docker Pulls](https://img.shields.io/docker/pulls/pmcilreavy/azureeventgridsimulator) -A simulator that provides endpoints to mimic the functionality of [Azure Event Grid](https://azure.microsoft.com/en-au/services/event-grid/) topics and subscribers and is compatible with the `Microsoft.Azure.EventGrid` client library. NOTE: Currently only the `EventGrid` event schema is supported. Support for the `CloudEvent` schema may be added at a future date. +A simulator that provides endpoints to mimic the functionality of [Azure Event Grid](https://azure.microsoft.com/en-au/services/event-grid/) topics and subscribers and is compatible with the `Microsoft.Azure.EventGrid` client library. NOTE: Currently only the `EventGrid` event schema is supported. ## Configuration @@ -163,9 +163,14 @@ docker-compose up --build ` --detach ``` -## Using the Simulator +## Using the Simulator + +Once configured and running, requests are `posted` to a topic endpoint. There are two endpoints, one for each supported schemas. + +EventGridEvent (Default) : The endpoint of a topic will be in the form: `https://localhost:/api/events?api-version=2018-01-01`. +CloudEvent : The endpoint of a topic will be in the form: `https://localhost:/api/events/cloudevent?api-version=2018-01-01`. + -Once configured and running, requests are `posted` to a topic endpoint. The endpoint of a topic will be in the form: `https://localhost:/api/events?api-version=2018-01-01`. #### cURL Example @@ -262,7 +267,6 @@ It posts the payload to https://host:port and drops the query uri. All of the ex Some features that could be added if there was a need for them: - -- `CloudEvent` schema support. - Subscriber retries & dead lettering. https://docs.microsoft.com/en-us/azure/event-grid/delivery-and-retry - Certificate configuration in `appsettings.json`. - Subscriber token auth From a22e5537a9584a52a691b4ab857ad20eba2fb861 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 12 Sep 2022 10:42:40 +0100 Subject: [PATCH 4/9] change line ending to LF --- .../Controllers/NotificationController.cs | 114 +++++++++--------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/src/AzureEventGridSimulator/Controllers/NotificationController.cs b/src/AzureEventGridSimulator/Controllers/NotificationController.cs index 177001e..b9e9135 100644 --- a/src/AzureEventGridSimulator/Controllers/NotificationController.cs +++ b/src/AzureEventGridSimulator/Controllers/NotificationController.cs @@ -1,57 +1,57 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Azure.Messaging; -using AzureEventGridSimulator.Domain; -using AzureEventGridSimulator.Domain.Commands; -using AzureEventGridSimulator.Domain.Entities; -using AzureEventGridSimulator.Infrastructure.Extensions; -using AzureEventGridSimulator.Infrastructure.Settings; -using MediatR; -using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; - -namespace AzureEventGridSimulator.Controllers; - -[Route("/api/events")] -[ApiVersion(Constants.SupportedApiVersion)] -[ApiController] -public class NotificationController : ControllerBase -{ - private readonly IMediator _mediator; - private readonly SimulatorSettings _simulatorSettings; - - public NotificationController(SimulatorSettings simulatorSettings, - IMediator mediator) - { - _mediator = mediator; - _simulatorSettings = simulatorSettings; - } - - [HttpPost] - public async Task Post() - { - var topicSettingsForCurrentRequestPort = _simulatorSettings.Topics.First(t => t.Port == HttpContext.Request.Host.Port); - - var eventsFromCurrentRequestBody = JsonConvert.DeserializeObject(await HttpContext.RequestBody()); - - await _mediator.Send(new SendNotificationEventsToSubscriberCommand(eventsFromCurrentRequestBody, topicSettingsForCurrentRequestPort)); - - return Ok(); - } - - - [Route("cloudevent")] - [HttpPost] - public async Task PostCloudEvent() - { - - var topicSettingsForCurrentRequestPort = _simulatorSettings.Topics.First(t => t.Port == HttpContext.Request.Host.Port); - - var eventsFromCurrentRequestBody = JsonConvert.DeserializeObject(await HttpContext.RequestBody()); - - await _mediator.Send(new SendNotificationCloudEventsToSubscriberCommand(eventsFromCurrentRequestBody, topicSettingsForCurrentRequestPort)); - - return Ok(); - } -} +using System; +using System.Linq; +using System.Threading.Tasks; +using Azure.Messaging; +using AzureEventGridSimulator.Domain; +using AzureEventGridSimulator.Domain.Commands; +using AzureEventGridSimulator.Domain.Entities; +using AzureEventGridSimulator.Infrastructure.Extensions; +using AzureEventGridSimulator.Infrastructure.Settings; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace AzureEventGridSimulator.Controllers; + +[Route("/api/events")] +[ApiVersion(Constants.SupportedApiVersion)] +[ApiController] +public class NotificationController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly SimulatorSettings _simulatorSettings; + + public NotificationController(SimulatorSettings simulatorSettings, + IMediator mediator) + { + _mediator = mediator; + _simulatorSettings = simulatorSettings; + } + + [HttpPost] + public async Task Post() + { + var topicSettingsForCurrentRequestPort = _simulatorSettings.Topics.First(t => t.Port == HttpContext.Request.Host.Port); + + var eventsFromCurrentRequestBody = JsonConvert.DeserializeObject(await HttpContext.RequestBody()); + + await _mediator.Send(new SendNotificationEventsToSubscriberCommand(eventsFromCurrentRequestBody, topicSettingsForCurrentRequestPort)); + + return Ok(); + } + + + [Route("cloudevent")] + [HttpPost] + public async Task PostCloudEvent() + { + + var topicSettingsForCurrentRequestPort = _simulatorSettings.Topics.First(t => t.Port == HttpContext.Request.Host.Port); + + var eventsFromCurrentRequestBody = JsonConvert.DeserializeObject(await HttpContext.RequestBody()); + + await _mediator.Send(new SendNotificationCloudEventsToSubscriberCommand(eventsFromCurrentRequestBody, topicSettingsForCurrentRequestPort)); + + return Ok(); + } +} From a7995730b5cc9cd271b990feedd4b6d2c83bcb51 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 12 Sep 2022 10:44:21 +0100 Subject: [PATCH 5/9] set line ending to LF and update readme --- README.md | 24 +- .../AzureEventGridSimulator.csproj | 1 - .../Controllers/NotificationController.cs | 4 +- ...onCloudEventsToSubscriberCommandHandler.cs | 280 +++++----- ...icationEventsToSubscriberCommandHandler.cs | 4 +- .../Middleware/EventGridMiddleware.cs | 503 +++++++++--------- 6 files changed, 409 insertions(+), 407 deletions(-) diff --git a/README.md b/README.md index 1a2efbf..7204512 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,9 @@ An example of one topic with one subscriber is shown below. #### Subscription Validation -When a subscription is added to Azure Event Grid it first sends a validation event to the subscription endpoint. The validation event contains a `validationCode` which the subscription endpoint must echo back. If this does not occur then Azure Event Grid will not enable the subscription. +When a subscription is added to Azure Event Grid it first sends a validation event to the subscription endpoint. The validation event contains a `validationCode` which the subscription endpoint must echo back. If this does not occur then Azure Event Grid will not enable the subscription. + +Validation is not supported for the cloudevent schema. More information about subscription validation can be found at [https://docs.microsoft.com/en-us/azure/event-grid/webhook-event-delivery](https://docs.microsoft.com/en-us/azure/event-grid/webhook-event-delivery). @@ -175,10 +177,10 @@ CloudEvent : The endpoint of a topic will be in the form: `https://localhost: - diff --git a/src/AzureEventGridSimulator/Controllers/NotificationController.cs b/src/AzureEventGridSimulator/Controllers/NotificationController.cs index b9e9135..e0ba34d 100644 --- a/src/AzureEventGridSimulator/Controllers/NotificationController.cs +++ b/src/AzureEventGridSimulator/Controllers/NotificationController.cs @@ -1,7 +1,5 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; -using Azure.Messaging; using AzureEventGridSimulator.Domain; using AzureEventGridSimulator.Domain.Commands; using AzureEventGridSimulator.Domain.Entities; diff --git a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs index 3a65d2f..b8864e5 100644 --- a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs +++ b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs @@ -1,143 +1,137 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Azure.Messaging; -using AzureEventGridSimulator.Domain.Entities; -using AzureEventGridSimulator.Infrastructure.Extensions; -using AzureEventGridSimulator.Infrastructure.Settings; -using MediatR; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; - -namespace AzureEventGridSimulator.Domain.Commands; - -// ReSharper disable once UnusedMember.Global -public class SendNotificationCloudEventsToSubscriberCommandHandler : AsyncRequestHandler -{ - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger _logger; - - public SendNotificationCloudEventsToSubscriberCommandHandler(IHttpClientFactory httpClientFactory, ILogger logger) - { - _httpClientFactory = httpClientFactory; - _logger = logger; - } - - protected override Task Handle(SendNotificationCloudEventsToSubscriberCommand request, CancellationToken cancellationToken) - { - _logger.LogInformation("{EventCount} event(s) received on topic '{TopicName}'", request.Events.Length, request.Topic.Name); - - foreach (var eventGridEvent in request.Events) - { - //eventGridEvent.Topic = $"/subscriptions/{Guid.Empty:D}/resourceGroups/eventGridSimulator/providers/Microsoft.EventGrid/topics/{request.Topic.Name}"; - //eventGridEvent.MetadataVersion = "1"; - } - - if (!request.Topic.Subscribers.Any()) - { - _logger.LogWarning("'{TopicName}' has no subscribers so {EventCount} event(s) could not be forwarded", request.Topic.Name, request.Events.Length); - } - else if (request.Topic.Subscribers.All(o => o.Disabled)) - { - _logger.LogWarning("'{TopicName}' has no enabled subscribers so {EventCount} event(s) could not be forwarded", request.Topic.Name, request.Events.Length); - } - else - { - var eventsFilteredOutByAllSubscribers = request.Events - .Where(e => request.Topic.Subscribers.All(s => !s.Filter.AcceptsEvent(e))) - .ToArray(); - - if (eventsFilteredOutByAllSubscribers.Any()) - { - foreach (var eventFilteredOutByAllSubscribers in eventsFilteredOutByAllSubscribers) - { - _logger.LogWarning("All subscribers of topic '{TopicName}' filtered out event {EventId}", - request.Topic.Name, - eventFilteredOutByAllSubscribers.Id); - } - } - else - { - foreach (var subscription in request.Topic.Subscribers) - { -#pragma warning disable 4014 - SendToSubscriber(subscription, request.Events, request.Topic.Name); -#pragma warning restore 4014 - } - } - } - - return Task.CompletedTask; - } - - private async Task SendToSubscriber(SubscriptionSettings subscription, IEnumerable events, string topicName) - { - try - { - if (subscription.Disabled) - { - _logger.LogWarning("Subscription '{SubscriberName}' on topic '{TopicName}' is disabled and so Notification was skipped", subscription.Name, topicName); - return; - } - - if (!subscription.DisableValidation && - subscription.ValidationStatus != SubscriptionValidationStatus.ValidationSuccessful) - { - _logger.LogWarning("Subscription '{SubscriberName}' on topic '{TopicName}' can't receive events. It's still pending validation", subscription.Name, topicName); - return; - } - - _logger.LogDebug("Sending to subscriber '{SubscriberName}' on topic '{TopicName}'", subscription.Name, topicName); - - // "Event Grid sends the events to subscribers in an array that has a single event. This behaviour may change in the future." - // https://docs.microsoft.com/en-us/azure/event-grid/event-schema - foreach (var evt in events) - { - if (subscription.Filter.AcceptsEvent(evt)) - { - var json = JsonConvert.SerializeObject(new[] { evt }, Formatting.Indented); - using var content = new StringContent(json, Encoding.UTF8, "application/json"); - var httpClient = _httpClientFactory.CreateClient(); - httpClient.DefaultRequestHeaders.Add(Constants.AegEventTypeHeader, Constants.NotificationEventType); - httpClient.DefaultRequestHeaders.Add(Constants.AegSubscriptionNameHeader, subscription.Name.ToUpperInvariant()); - //httpClient.DefaultRequestHeaders.Add(Constants.AegDataVersionHeader, evt.DataVersion); - //httpClient.DefaultRequestHeaders.Add(Constants.AegMetadataVersionHeader, evt.MetadataVersion); - httpClient.DefaultRequestHeaders.Add(Constants.AegDeliveryCountHeader, "0"); // TODO implement re-tries - httpClient.Timeout = TimeSpan.FromSeconds(60); - - await httpClient.PostAsync(subscription.Endpoint, content) - .ContinueWith(t => LogResult(t, evt, subscription, topicName)); - } - else - { - _logger.LogDebug("Event {EventId} filtered out for subscriber '{SubscriberName}'", evt.Id, subscription.Name); - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to send to subscriber '{SubscriberName}'", subscription.Name); - } - } - - private void LogResult(Task task, CloudEventGridEvent evt, SubscriptionSettings subscription, string topicName) - { - if (task.IsCompletedSuccessfully && task.Result.IsSuccessStatusCode) - { - _logger.LogDebug("Event {EventId} sent to subscriber '{SubscriberName}' on topic '{TopicName}' successfully", evt.Id, subscription.Name, topicName); - } - else - { - _logger.LogError(task.Exception?.GetBaseException(), - "Failed to send event {EventId} to subscriber '{SubscriberName}', '{TaskStatus}', '{Reason}'", - evt.Id, - subscription.Name, - task.Status.ToString(), - task.Result?.ReasonPhrase); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Azure.Messaging; +using AzureEventGridSimulator.Domain.Entities; +using AzureEventGridSimulator.Infrastructure.Extensions; +using AzureEventGridSimulator.Infrastructure.Settings; +using MediatR; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace AzureEventGridSimulator.Domain.Commands; + +// ReSharper disable once UnusedMember.Global +public class SendNotificationCloudEventsToSubscriberCommandHandler : AsyncRequestHandler +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + + public SendNotificationCloudEventsToSubscriberCommandHandler(IHttpClientFactory httpClientFactory, ILogger logger) + { + _httpClientFactory = httpClientFactory; + _logger = logger; + } + + protected override Task Handle(SendNotificationCloudEventsToSubscriberCommand request, CancellationToken cancellationToken) + { + _logger.LogInformation("{EventCount} event(s) received on topic '{TopicName}'", request.Events.Length, request.Topic.Name); + + if (!request.Topic.Subscribers.Any()) + { + _logger.LogWarning("'{TopicName}' has no subscribers so {EventCount} event(s) could not be forwarded", request.Topic.Name, request.Events.Length); + } + else if (request.Topic.Subscribers.All(o => o.Disabled)) + { + _logger.LogWarning("'{TopicName}' has no enabled subscribers so {EventCount} event(s) could not be forwarded", request.Topic.Name, request.Events.Length); + } + else + { + var eventsFilteredOutByAllSubscribers = request.Events + .Where(e => request.Topic.Subscribers.All(s => !s.Filter.AcceptsEvent(e))) + .ToArray(); + + if (eventsFilteredOutByAllSubscribers.Any()) + { + foreach (var eventFilteredOutByAllSubscribers in eventsFilteredOutByAllSubscribers) + { + _logger.LogWarning("All subscribers of topic '{TopicName}' filtered out event {EventId}", + request.Topic.Name, + eventFilteredOutByAllSubscribers.Id); + } + } + else + { + foreach (var subscription in request.Topic.Subscribers) + { +#pragma warning disable 4014 + SendToSubscriber(subscription, request.Events, request.Topic.Name); +#pragma warning restore 4014 + } + } + } + + return Task.CompletedTask; + } + + private async Task SendToSubscriber(SubscriptionSettings subscription, IEnumerable events, string topicName) + { + try + { + if (subscription.Disabled) + { + _logger.LogWarning("Subscription '{SubscriberName}' on topic '{TopicName}' is disabled and so Notification was skipped", subscription.Name, topicName); + return; + } + + if (!subscription.DisableValidation && + subscription.ValidationStatus != SubscriptionValidationStatus.ValidationSuccessful) + { + _logger.LogWarning("Subscription '{SubscriberName}' on topic '{TopicName}' can't receive events. It's still pending validation", subscription.Name, topicName); + return; + } + + _logger.LogDebug("Sending to subscriber '{SubscriberName}' on topic '{TopicName}'", subscription.Name, topicName); + + // "Event Grid sends the events to subscribers in an array that has a single event. This behaviour may change in the future." + // https://docs.microsoft.com/en-us/azure/event-grid/event-schema + foreach (var evt in events) + { + if (subscription.Filter.AcceptsEvent(evt)) + { + var json = JsonConvert.SerializeObject(new[] { evt }, Formatting.Indented); + using var content = new StringContent(json, Encoding.UTF8, "application/json"); + var httpClient = _httpClientFactory.CreateClient(); + httpClient.DefaultRequestHeaders.Add(Constants.AegEventTypeHeader, Constants.NotificationEventType); + httpClient.DefaultRequestHeaders.Add(Constants.AegSubscriptionNameHeader, subscription.Name.ToUpperInvariant()); + //httpClient.DefaultRequestHeaders.Add(Constants.AegDataVersionHeader, evt.DataVersion); + //httpClient.DefaultRequestHeaders.Add(Constants.AegMetadataVersionHeader, evt.MetadataVersion); + httpClient.DefaultRequestHeaders.Add(Constants.AegDeliveryCountHeader, "0"); // TODO implement re-tries + httpClient.Timeout = TimeSpan.FromSeconds(60); + + await httpClient.PostAsync(subscription.Endpoint, content) + .ContinueWith(t => LogResult(t, evt, subscription, topicName)); + } + else + { + _logger.LogDebug("Event {EventId} filtered out for subscriber '{SubscriberName}'", evt.Id, subscription.Name); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send to subscriber '{SubscriberName}'", subscription.Name); + } + } + + private void LogResult(Task task, CloudEventGridEvent evt, SubscriptionSettings subscription, string topicName) + { + if (task.IsCompletedSuccessfully && task.Result.IsSuccessStatusCode) + { + _logger.LogDebug("Event {EventId} sent to subscriber '{SubscriberName}' on topic '{TopicName}' successfully", evt.Id, subscription.Name, topicName); + } + else + { + _logger.LogError(task.Exception?.GetBaseException(), + "Failed to send event {EventId} to subscriber '{SubscriberName}', '{TaskStatus}', '{Reason}'", + evt.Id, + subscription.Name, + task.Status.ToString(), + task.Result?.ReasonPhrase); + } + } +} diff --git a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs index 74eb9c3..2e444ae 100644 --- a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs +++ b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs @@ -97,8 +97,8 @@ private async Task SendToSubscriber(SubscriptionSettings subscription, IEnumerab foreach (var evt in events) { if (subscription.Filter.AcceptsEvent(evt)) - { - + { + // write to azurite instead? var json = JsonConvert.SerializeObject(new[] { evt }, Formatting.Indented); using var content = new StringContent(json, Encoding.UTF8, "application/json"); diff --git a/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs b/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs index 2fcea13..2b51147 100644 --- a/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs +++ b/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs @@ -1,255 +1,248 @@ -using System; -using System.Diagnostics; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Azure.Messaging; -using AzureEventGridSimulator.Domain.Entities; -using AzureEventGridSimulator.Domain.Services; -using AzureEventGridSimulator.Infrastructure.Extensions; -using AzureEventGridSimulator.Infrastructure.Settings; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; - -namespace AzureEventGridSimulator.Infrastructure.Middleware; - -public class EventGridMiddleware -{ - private readonly RequestDelegate _next; - - public EventGridMiddleware(RequestDelegate next) - { - _next = next; - } - - // ReSharper disable once UnusedMember.Global - public async Task InvokeAsync(HttpContext context, - SimulatorSettings simulatorSettings, - SasKeyValidator sasHeaderValidator, - ILogger logger) - { - logger.LogInformation("Headers:"); - logger.LogInformation(JsonConvert.SerializeObject(context.Request.Headers)); - logger.LogInformation("Path:"); - logger.LogInformation(context.Request.Path); - logger.LogInformation("Query string: "); - logger.LogInformation(JsonConvert.SerializeObject(context.Request.QueryString)); - - if (IsNotificationRequest(context)) - { - await ValidateNotificationRequest(context, simulatorSettings, sasHeaderValidator, logger); - return; - } - - if (IsCloudEventNotificationRequest(context)) - { - await ValidateNotificationCloudEventRequest(context, simulatorSettings, sasHeaderValidator, logger); - return; - } - - if (IsValidationRequest(context)) - { - await ValidateSubscriptionValidationRequest(context); - return; - } - - //if (IsCloudEventNotificationRequest(context)) - //{ - // await ValidateSubscriptionValidationRequest(context); - // return; - - //} - - // This is the end of the line. - await context.WriteErrorResponse(HttpStatusCode.BadRequest, "Request not supported.", null); - } - - private async Task ValidateSubscriptionValidationRequest(HttpContext context) - { - var id = context.Request.Query["id"]; - - if (string.IsNullOrWhiteSpace(id)) - { - await context.WriteErrorResponse(HttpStatusCode.BadRequest, "The request did not contain a validation code.", null); - return; - } - - await _next(context); - } - - private async Task ValidateNotificationRequest(HttpContext context, - SimulatorSettings simulatorSettings, - SasKeyValidator sasHeaderValidator, - ILogger logger) - { - var topic = simulatorSettings.Topics.First(t => t.Port == context.Request.Host.Port); - - // - // Validate the key/ token supplied in the header. - // - if (!string.IsNullOrWhiteSpace(topic.Key) && - !sasHeaderValidator.IsValid(context.Request.Headers, topic.Key)) - { - await context.WriteErrorResponse(HttpStatusCode.Unauthorized, "The request did not contain a valid aeg-sas-key or aeg-sas-token.", null); - return; - } - - context.Request.EnableBuffering(); - var requestBody = await context.RequestBody(); - var events = JsonConvert.DeserializeObject(requestBody); - - // - // Validate the overall body size and the size of each event. - // - const int maximumAllowedOverallMessageSizeInBytes = 1536000; - const int maximumAllowedEventGridEventSizeInBytes = 66560; - - if (requestBody.Length > maximumAllowedOverallMessageSizeInBytes) - { - logger.LogError("Payload is larger than the allowed maximum"); - - await context.WriteErrorResponse(HttpStatusCode.RequestEntityTooLarge, "Payload is larger than the allowed maximum.", null); - return; - } - - if (events != null) - { - foreach (var evt in events) - { - var eventSize = JsonConvert.SerializeObject(evt, Formatting.None).Length; - - if (eventSize <= maximumAllowedEventGridEventSizeInBytes) - { - continue; - } - - logger.LogError("Event is larger than the allowed maximum"); - - await context.WriteErrorResponse(HttpStatusCode.RequestEntityTooLarge, "Event is larger than the allowed maximum.", null); - return; - } - - // - // Validate the properties of each event. - // - foreach (var eventGridEvent in events) - { - try - { - eventGridEvent.Validate(); - } - catch (InvalidOperationException ex) - { - logger.LogError(ex, "Event was not valid"); - - await context.WriteErrorResponse(HttpStatusCode.BadRequest, ex.Message, null); - return; - } - } - } - - await _next(context); - } - - - private async Task ValidateNotificationCloudEventRequest(HttpContext context, - SimulatorSettings simulatorSettings, - SasKeyValidator sasHeaderValidator, - ILogger logger) - { - var topic = simulatorSettings.Topics.First(t => t.Port == context.Request.Host.Port); - - // - // Validate the key/ token supplied in the header. - // - if (!string.IsNullOrWhiteSpace(topic.Key) && - !sasHeaderValidator.IsValid(context.Request.Headers, topic.Key)) - { - await context.WriteErrorResponse(HttpStatusCode.Unauthorized, "The request did not contain a valid aeg-sas-key or aeg-sas-token.", null); - return; - } - - context.Request.EnableBuffering(); - var requestBody = await context.RequestBody(); - var events = JsonConvert.DeserializeObject(requestBody); - - // - // Validate the overall body size and the size of each event. - // - const int maximumAllowedOverallMessageSizeInBytes = 1536000; - const int maximumAllowedEventGridEventSizeInBytes = 66560; - - if (requestBody.Length > maximumAllowedOverallMessageSizeInBytes) - { - logger.LogError("Payload is larger than the allowed maximum"); - - await context.WriteErrorResponse(HttpStatusCode.RequestEntityTooLarge, "Payload is larger than the allowed maximum.", null); - return; - } - - if (events != null) - { - foreach (var evt in events) - { - var eventSize = JsonConvert.SerializeObject(evt, Formatting.None).Length; - - if (eventSize <= maximumAllowedEventGridEventSizeInBytes) - { - continue; - } - - logger.LogError("Event is larger than the allowed maximum"); - - await context.WriteErrorResponse(HttpStatusCode.RequestEntityTooLarge, "Event is larger than the allowed maximum.", null); - return; - } - - // - // Validate the properties of each event. - // - foreach (var eventGridEvent in events) - { - try - { - CloudEventValidateService.Validate(eventGridEvent); - - } - catch (InvalidOperationException ex) - { - logger.LogError(ex, "Event was not valid"); - - await context.WriteErrorResponse(HttpStatusCode.BadRequest, ex.Message, null); - return; - } - } - } - - await _next(context); - } - - - private static bool IsNotificationRequest(HttpContext context) - { - return context.Request.Headers.Keys.Any(k => string.Equals(k, "Content-Type", StringComparison.OrdinalIgnoreCase)) && - context.Request.Headers["Content-Type"].Any(v => !string.IsNullOrWhiteSpace(v) && v.Contains("application/json", StringComparison.OrdinalIgnoreCase)) && - context.Request.Method == HttpMethods.Post && - string.Equals(context.Request.Path, "/api/events", StringComparison.OrdinalIgnoreCase); - } - - private static bool IsCloudEventNotificationRequest(HttpContext context) - { - return context.Request.Headers.Keys.Any(k => string.Equals(k, "Content-Type", StringComparison.OrdinalIgnoreCase)) && - context.Request.Headers["Content-Type"].Any(v => !string.IsNullOrWhiteSpace(v) && (v.Contains("application/json", StringComparison.OrdinalIgnoreCase) || v.Contains("application/cloudevents-batch+json", StringComparison.OrdinalIgnoreCase))) && - context.Request.Method == HttpMethods.Post && - string.Equals(context.Request.Path, "/api/events/cloudevent", StringComparison.OrdinalIgnoreCase); - } - - private static bool IsValidationRequest(HttpContext context) - { - return context.Request.Method == HttpMethods.Get && - string.Equals(context.Request.Path, "/validate", StringComparison.OrdinalIgnoreCase) && - context.Request.Query.Keys.Any(k => string.Equals(k, "id", StringComparison.OrdinalIgnoreCase)) && - Guid.TryParse(context.Request.Query["id"], out _); - } -} +using System; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Azure.Messaging; +using AzureEventGridSimulator.Domain.Entities; +using AzureEventGridSimulator.Domain.Services; +using AzureEventGridSimulator.Infrastructure.Extensions; +using AzureEventGridSimulator.Infrastructure.Settings; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace AzureEventGridSimulator.Infrastructure.Middleware; + +public class EventGridMiddleware +{ + private readonly RequestDelegate _next; + + public EventGridMiddleware(RequestDelegate next) + { + _next = next; + } + + // ReSharper disable once UnusedMember.Global + public async Task InvokeAsync(HttpContext context, + SimulatorSettings simulatorSettings, + SasKeyValidator sasHeaderValidator, + ILogger logger) + { + logger.LogInformation("Headers:"); + logger.LogInformation(JsonConvert.SerializeObject(context.Request.Headers)); + logger.LogInformation("Path:"); + logger.LogInformation(context.Request.Path); + logger.LogInformation("Query string: "); + logger.LogInformation(JsonConvert.SerializeObject(context.Request.QueryString)); + + if (IsNotificationRequest(context)) + { + await ValidateNotificationRequest(context, simulatorSettings, sasHeaderValidator, logger); + return; + } + + if (IsCloudEventNotificationRequest(context)) + { + await ValidateNotificationCloudEventRequest(context, simulatorSettings, sasHeaderValidator, logger); + return; + } + + if (IsValidationRequest(context)) + { + await ValidateSubscriptionValidationRequest(context); + return; + } + + // This is the end of the line. + await context.WriteErrorResponse(HttpStatusCode.BadRequest, "Request not supported.", null); + } + + private async Task ValidateSubscriptionValidationRequest(HttpContext context) + { + var id = context.Request.Query["id"]; + + if (string.IsNullOrWhiteSpace(id)) + { + await context.WriteErrorResponse(HttpStatusCode.BadRequest, "The request did not contain a validation code.", null); + return; + } + + await _next(context); + } + + private async Task ValidateNotificationRequest(HttpContext context, + SimulatorSettings simulatorSettings, + SasKeyValidator sasHeaderValidator, + ILogger logger) + { + var topic = simulatorSettings.Topics.First(t => t.Port == context.Request.Host.Port); + + // + // Validate the key/ token supplied in the header. + // + if (!string.IsNullOrWhiteSpace(topic.Key) && + !sasHeaderValidator.IsValid(context.Request.Headers, topic.Key)) + { + await context.WriteErrorResponse(HttpStatusCode.Unauthorized, "The request did not contain a valid aeg-sas-key or aeg-sas-token.", null); + return; + } + + context.Request.EnableBuffering(); + var requestBody = await context.RequestBody(); + var events = JsonConvert.DeserializeObject(requestBody); + + // + // Validate the overall body size and the size of each event. + // + const int maximumAllowedOverallMessageSizeInBytes = 1536000; + const int maximumAllowedEventGridEventSizeInBytes = 66560; + + if (requestBody.Length > maximumAllowedOverallMessageSizeInBytes) + { + logger.LogError("Payload is larger than the allowed maximum"); + + await context.WriteErrorResponse(HttpStatusCode.RequestEntityTooLarge, "Payload is larger than the allowed maximum.", null); + return; + } + + if (events != null) + { + foreach (var evt in events) + { + var eventSize = JsonConvert.SerializeObject(evt, Formatting.None).Length; + + if (eventSize <= maximumAllowedEventGridEventSizeInBytes) + { + continue; + } + + logger.LogError("Event is larger than the allowed maximum"); + + await context.WriteErrorResponse(HttpStatusCode.RequestEntityTooLarge, "Event is larger than the allowed maximum.", null); + return; + } + + // + // Validate the properties of each event. + // + foreach (var eventGridEvent in events) + { + try + { + eventGridEvent.Validate(); + } + catch (InvalidOperationException ex) + { + logger.LogError(ex, "Event was not valid"); + + await context.WriteErrorResponse(HttpStatusCode.BadRequest, ex.Message, null); + return; + } + } + } + + await _next(context); + } + + + private async Task ValidateNotificationCloudEventRequest(HttpContext context, + SimulatorSettings simulatorSettings, + SasKeyValidator sasHeaderValidator, + ILogger logger) + { + var topic = simulatorSettings.Topics.First(t => t.Port == context.Request.Host.Port); + + // + // Validate the key/ token supplied in the header. + // + if (!string.IsNullOrWhiteSpace(topic.Key) && + !sasHeaderValidator.IsValid(context.Request.Headers, topic.Key)) + { + await context.WriteErrorResponse(HttpStatusCode.Unauthorized, "The request did not contain a valid aeg-sas-key or aeg-sas-token.", null); + return; + } + + context.Request.EnableBuffering(); + var requestBody = await context.RequestBody(); + var events = JsonConvert.DeserializeObject(requestBody); + + // + // Validate the overall body size and the size of each event. + // + const int maximumAllowedOverallMessageSizeInBytes = 1536000; + const int maximumAllowedEventGridEventSizeInBytes = 66560; + + if (requestBody.Length > maximumAllowedOverallMessageSizeInBytes) + { + logger.LogError("Payload is larger than the allowed maximum"); + + await context.WriteErrorResponse(HttpStatusCode.RequestEntityTooLarge, "Payload is larger than the allowed maximum.", null); + return; + } + + if (events != null) + { + foreach (var evt in events) + { + var eventSize = JsonConvert.SerializeObject(evt, Formatting.None).Length; + + if (eventSize <= maximumAllowedEventGridEventSizeInBytes) + { + continue; + } + + logger.LogError("Event is larger than the allowed maximum"); + + await context.WriteErrorResponse(HttpStatusCode.RequestEntityTooLarge, "Event is larger than the allowed maximum.", null); + return; + } + + // + // Validate the properties of each event. + // + foreach (var eventGridEvent in events) + { + try + { + CloudEventValidateService.Validate(eventGridEvent); + + } + catch (InvalidOperationException ex) + { + logger.LogError(ex, "Event was not valid"); + + await context.WriteErrorResponse(HttpStatusCode.BadRequest, ex.Message, null); + return; + } + } + } + + await _next(context); + } + + + private static bool IsNotificationRequest(HttpContext context) + { + return context.Request.Headers.Keys.Any(k => string.Equals(k, "Content-Type", StringComparison.OrdinalIgnoreCase)) && + context.Request.Headers["Content-Type"].Any(v => !string.IsNullOrWhiteSpace(v) && v.Contains("application/json", StringComparison.OrdinalIgnoreCase)) && + context.Request.Method == HttpMethods.Post && + string.Equals(context.Request.Path, "/api/events", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsCloudEventNotificationRequest(HttpContext context) + { + return context.Request.Headers.Keys.Any(k => string.Equals(k, "Content-Type", StringComparison.OrdinalIgnoreCase)) && + context.Request.Headers["Content-Type"].Any(v => !string.IsNullOrWhiteSpace(v) && (v.Contains("application/json", StringComparison.OrdinalIgnoreCase) || v.Contains("application/cloudevents-batch+json", StringComparison.OrdinalIgnoreCase))) && + context.Request.Method == HttpMethods.Post && + string.Equals(context.Request.Path, "/api/events/cloudevent", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsValidationRequest(HttpContext context) + { + return context.Request.Method == HttpMethods.Get && + string.Equals(context.Request.Path, "/validate", StringComparison.OrdinalIgnoreCase) && + context.Request.Query.Keys.Any(k => string.Equals(k, "id", StringComparison.OrdinalIgnoreCase)) && + Guid.TryParse(context.Request.Query["id"], out _); + } +} From 7a01a74c042646e53d5bc5789c446bd1ffc4d42a Mon Sep 17 00:00:00 2001 From: James Date: Mon, 12 Sep 2022 10:50:14 +0100 Subject: [PATCH 6/9] cleanup validation --- ...icationEventsToSubscriberCommandHandler.cs | 1 - .../Domain/Entities/CloudEventGridEvent.cs | 43 +++++++++++++- .../Services/CloudEventValidateService.cs | 56 ------------------- .../Middleware/EventGridMiddleware.cs | 2 +- 4 files changed, 43 insertions(+), 59 deletions(-) delete mode 100644 src/AzureEventGridSimulator/Domain/Services/CloudEventValidateService.cs diff --git a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs index 2e444ae..f0fa55c 100644 --- a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs +++ b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommandHandler.cs @@ -99,7 +99,6 @@ private async Task SendToSubscriber(SubscriptionSettings subscription, IEnumerab if (subscription.Filter.AcceptsEvent(evt)) { - // write to azurite instead? var json = JsonConvert.SerializeObject(new[] { evt }, Formatting.Indented); using var content = new StringContent(json, Encoding.UTF8, "application/json"); var httpClient = _httpClientFactory.CreateClient(); diff --git a/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs b/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs index 18e4011..66ea24c 100644 --- a/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs +++ b/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs @@ -31,5 +31,46 @@ public class CloudEventGridEvent public string DataContentType { get; set; } [DataMember(Name = "subject")] [JsonProperty("subject")] - public string Subject { get; set; } + public string Subject { get; set; } + + public void Validate() + { + if (string.IsNullOrWhiteSpace(Id)) + { + throw new InvalidOperationException($"Required property '{nameof(Id)}' was not set."); + } + + if (string.IsNullOrWhiteSpace(Source)) + { + throw new InvalidOperationException($"Required property '{nameof(Source)}' was not set."); + } + + if (string.IsNullOrWhiteSpace(Type)) + { + throw new InvalidOperationException($"Required property '{nameof(Type)}' was not set."); + } + + if (string.IsNullOrWhiteSpace(Data_Base64)) + { + throw new InvalidOperationException($"Required property '{nameof(Data_Base64)}' was not set."); + } + + if (string.IsNullOrWhiteSpace(DataSchema)) + { + throw new InvalidOperationException($"Required property '{nameof(DataSchema)}' was not set."); + } + + if (string.IsNullOrWhiteSpace(DataContentType)) + { + throw new InvalidOperationException($"Required property '{nameof(DataContentType)}' was not set."); + } + + if (string.IsNullOrWhiteSpace(Subject)) + { + throw new InvalidOperationException($"Required property '{nameof(Subject)}' was not set."); + } + + + } + } diff --git a/src/AzureEventGridSimulator/Domain/Services/CloudEventValidateService.cs b/src/AzureEventGridSimulator/Domain/Services/CloudEventValidateService.cs deleted file mode 100644 index ce43281..0000000 --- a/src/AzureEventGridSimulator/Domain/Services/CloudEventValidateService.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using Azure.Messaging; -using AzureEventGridSimulator.Domain.Entities; -using Microsoft.Extensions.Logging; - -namespace AzureEventGridSimulator.Domain.Services; - -public static class CloudEventValidateService -{ - public static void Validate(CloudEventGridEvent @event) - { - if (string.IsNullOrWhiteSpace(@event.Id)) - { - throw new InvalidOperationException($"Required property '{nameof(@event.Id)}' was not set."); - } - - if (string.IsNullOrWhiteSpace(@event.Subject)) - { - throw new InvalidOperationException($"Required property '{nameof(@event.Subject)}' was not set."); - } - - if (string.IsNullOrWhiteSpace(@event.Type)) - { - throw new InvalidOperationException($"Required property '{nameof(@event.Type)}' was not set."); - } - //} - - //if (string.IsNullOrWhiteSpace(EventTime)) - //{ - // throw new InvalidOperationException($"Required property '{nameof(EventTime)}' was not set."); - //} - - //if (!EventTimeIsValid) - //{ - // throw new InvalidOperationException($"The event time property '{nameof(EventTime)}' was not a valid date/time."); - //} - - //if (EventTimeParsed.Kind == DateTimeKind.Unspecified) - //{ - // throw new InvalidOperationException($"Property '{nameof(EventTime)}' must be either Local or UTC."); - //} - - //if (MetadataVersion != null && MetadataVersion != "1") - //{ - // throw new - // InvalidOperationException($"Property '{nameof(MetadataVersion)}' was found to be set to '{MetadataVersion}', but was expected to either be null or be set to 1."); - //} - - //if (!string.IsNullOrEmpty(Topic)) - //{ - // throw new InvalidOperationException($"Property '{nameof(Topic)}' was found to be set to '{Topic}', but was expected to either be null/empty."); - //} - - } - -} diff --git a/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs b/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs index 2b51147..0e1667d 100644 --- a/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs +++ b/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs @@ -205,7 +205,7 @@ private async Task ValidateNotificationCloudEventRequest(HttpContext context, { try { - CloudEventValidateService.Validate(eventGridEvent); + eventGridEvent.Validate(); } catch (InvalidOperationException ex) From a713815fcf99f959bef80ebd61e40e79caafdf08 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 12 Sep 2022 10:55:22 +0100 Subject: [PATCH 7/9] cleanup code and fix line ending --- .../AzureEventGridSimulator.csproj | 1 - ...ificationCloudEventsToSubscriberCommand.cs | 3 +- ...onCloudEventsToSubscriberCommandHandler.cs | 3 - ...ndNotificationEventsToSubscriberCommand.cs | 3 +- .../Domain/Entities/CloudEventGridEvent.cs | 3 +- ...ptionCloudEventSettingsFilterExtensions.cs | 9 - .../Middleware/EventGridMiddleware.cs | 10 - src/AzureEventGridSimulator/Program.cs | 478 +++++++++--------- 8 files changed, 242 insertions(+), 268 deletions(-) diff --git a/src/AzureEventGridSimulator/AzureEventGridSimulator.csproj b/src/AzureEventGridSimulator/AzureEventGridSimulator.csproj index d1f105f..a5a02dd 100644 --- a/src/AzureEventGridSimulator/AzureEventGridSimulator.csproj +++ b/src/AzureEventGridSimulator/AzureEventGridSimulator.csproj @@ -13,7 +13,6 @@ - diff --git a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommand.cs b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommand.cs index 0483a64..607a636 100644 --- a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommand.cs +++ b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommand.cs @@ -1,5 +1,4 @@ -using Azure.Messaging; -using AzureEventGridSimulator.Domain.Entities; +using AzureEventGridSimulator.Domain.Entities; using AzureEventGridSimulator.Infrastructure.Settings; using MediatR; diff --git a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs index b8864e5..3efa7b3 100644 --- a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs +++ b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationCloudEventsToSubscriberCommandHandler.cs @@ -5,7 +5,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Azure.Messaging; using AzureEventGridSimulator.Domain.Entities; using AzureEventGridSimulator.Infrastructure.Extensions; using AzureEventGridSimulator.Infrastructure.Settings; @@ -98,8 +97,6 @@ private async Task SendToSubscriber(SubscriptionSettings subscription, IEnumerab var httpClient = _httpClientFactory.CreateClient(); httpClient.DefaultRequestHeaders.Add(Constants.AegEventTypeHeader, Constants.NotificationEventType); httpClient.DefaultRequestHeaders.Add(Constants.AegSubscriptionNameHeader, subscription.Name.ToUpperInvariant()); - //httpClient.DefaultRequestHeaders.Add(Constants.AegDataVersionHeader, evt.DataVersion); - //httpClient.DefaultRequestHeaders.Add(Constants.AegMetadataVersionHeader, evt.MetadataVersion); httpClient.DefaultRequestHeaders.Add(Constants.AegDeliveryCountHeader, "0"); // TODO implement re-tries httpClient.Timeout = TimeSpan.FromSeconds(60); diff --git a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommand.cs b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommand.cs index d84ee7f..c97978c 100644 --- a/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommand.cs +++ b/src/AzureEventGridSimulator/Domain/Commands/SendNotificationEventsToSubscriberCommand.cs @@ -1,5 +1,4 @@ -using Azure.Messaging; -using AzureEventGridSimulator.Domain.Entities; +using AzureEventGridSimulator.Domain.Entities; using AzureEventGridSimulator.Infrastructure.Settings; using MediatR; diff --git a/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs b/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs index 66ea24c..8bcdd80 100644 --- a/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs +++ b/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs @@ -69,8 +69,7 @@ public void Validate() { throw new InvalidOperationException($"Required property '{nameof(Subject)}' was not set."); } - - + } } diff --git a/src/AzureEventGridSimulator/Infrastructure/Extensions/SubscriptionCloudEventSettingsFilterExtensions.cs b/src/AzureEventGridSimulator/Infrastructure/Extensions/SubscriptionCloudEventSettingsFilterExtensions.cs index 8ca0ce6..95367d7 100644 --- a/src/AzureEventGridSimulator/Infrastructure/Extensions/SubscriptionCloudEventSettingsFilterExtensions.cs +++ b/src/AzureEventGridSimulator/Infrastructure/Extensions/SubscriptionCloudEventSettingsFilterExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using Azure.Messaging; using AzureEventGridSimulator.Domain.Entities; using AzureEventGridSimulator.Infrastructure.Settings; using Newtonsoft.Json.Linq; @@ -157,10 +156,6 @@ private static bool TryGetValue(this CloudEventGridEvent gridEvent, string key, value = gridEvent.Id; retval = true; break; - //case nameof(gridEvent.Topic): - // value = gridEvent.Topic; - // retval = true; - // break; case nameof(gridEvent.Subject): value = gridEvent.Subject; retval = true; @@ -169,10 +164,6 @@ private static bool TryGetValue(this CloudEventGridEvent gridEvent, string key, value = gridEvent.Type; retval = true; break; - //case nameof(gridEvent.DataVersion): - // value = gridEvent.DataVersion; - // retval = true; - // break; case nameof(gridEvent.Data_Base64): value = gridEvent.Data_Base64; retval = true; diff --git a/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs b/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs index 0e1667d..2267820 100644 --- a/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs +++ b/src/AzureEventGridSimulator/Infrastructure/Middleware/EventGridMiddleware.cs @@ -1,11 +1,8 @@ using System; -using System.Diagnostics; using System.Linq; using System.Net; using System.Threading.Tasks; -using Azure.Messaging; using AzureEventGridSimulator.Domain.Entities; -using AzureEventGridSimulator.Domain.Services; using AzureEventGridSimulator.Infrastructure.Extensions; using AzureEventGridSimulator.Infrastructure.Settings; using Microsoft.AspNetCore.Http; @@ -29,13 +26,6 @@ public async Task InvokeAsync(HttpContext context, SasKeyValidator sasHeaderValidator, ILogger logger) { - logger.LogInformation("Headers:"); - logger.LogInformation(JsonConvert.SerializeObject(context.Request.Headers)); - logger.LogInformation("Path:"); - logger.LogInformation(context.Request.Path); - logger.LogInformation("Query string: "); - logger.LogInformation(JsonConvert.SerializeObject(context.Request.QueryString)); - if (IsNotificationRequest(context)) { await ValidateNotificationRequest(context, simulatorSettings, sasHeaderValidator, logger); diff --git a/src/AzureEventGridSimulator/Program.cs b/src/AzureEventGridSimulator/Program.cs index 76e484b..df6adfb 100644 --- a/src/AzureEventGridSimulator/Program.cs +++ b/src/AzureEventGridSimulator/Program.cs @@ -1,239 +1,239 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using AzureEventGridSimulator.Domain; -using AzureEventGridSimulator.Domain.Commands; -using AzureEventGridSimulator.Infrastructure; -using AzureEventGridSimulator.Infrastructure.Extensions; -using AzureEventGridSimulator.Infrastructure.Middleware; -using AzureEventGridSimulator.Infrastructure.Settings; -using MediatR; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Versioning; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Serilog; -using Serilog.Events; -using ILogger = Serilog.ILogger; - -[assembly: InternalsVisibleTo("AzureEventGridSimulator.Tests")] - -namespace AzureEventGridSimulator; - -public class Program -{ - public static async Task Main(string[] args) - { - try - { - // Build it and fire it up - var app = CreateWebHostBuilder(args) - .Build(); - - app.UseSerilogRequestLogging(options => { options.GetLevel = (_, _, _) => LogEventLevel.Debug; }); - app.UseEventGridMiddleware(); - app.UseRouting(); - app.UseEndpoints(e => { e.MapControllers(); }); - - await StartSimulator(app); - } - catch (Exception ex) - { - Log.Fatal(ex, "Failed to start the Azure Event Grid Simulator"); - } - finally - { - Log.CloseAndFlush(); - } - } - - public static async Task StartSimulator(WebApplication host, CancellationToken token = default) - { - try - { - await host.StartAsync(token) - .ContinueWith(_ => OnApplicationStarted(host, host.Lifetime), token) - .ConfigureAwait(false); - - await host.WaitForShutdownAsync(token).ConfigureAwait(false); - } - finally - { - await host.DisposeAsync().ConfigureAwait(false); - } - } - - private static async Task OnApplicationStarted(IApplicationBuilder app, IHostApplicationLifetime lifetime) - { - try - { - Log.Verbose("Started"); - - var simulatorSettings = app.ApplicationServices.GetService(); - - if (simulatorSettings is null) - { - Log.Fatal("Settings are not found. The application will now exit"); - lifetime.StopApplication(); - return; - } - - if (!simulatorSettings.Topics.Any()) - { - Log.Fatal("There are no configured topics. The application will now exit"); - lifetime.StopApplication(); - return; - } - - if (simulatorSettings.Topics.All(o => o.Disabled)) - { - Log.Fatal("All of the configured topics are disabled. The application will now exit"); - lifetime.StopApplication(); - return; - } - - var mediator = app.ApplicationServices.GetService(); - - if (mediator is null) - { - Log.Fatal("Required component was not found. The application will now exit"); - lifetime.StopApplication(); - return; - } - - await mediator.Send(new ValidateAllSubscriptionsCommand()); - - Log.Information("It's alive !"); - } - catch (Exception e) - { - Log.Fatal(e, "It died !"); - lifetime.StopApplication(); - } - } - - private static WebApplicationBuilder CreateWebHostBuilder(string[] args) - { - // Set up basic Console logger we can use to log to until we've finished building everything - Log.Logger = CreateBasicConsoleLogger(); - - // First thing's first. Build the configuration. - var configuration = BuildConfiguration(args); - - // Configure the web host builder - return ConfigureWebHost(args, configuration); - } - - private static ILogger CreateBasicConsoleLogger() - { - return new LoggerConfiguration() - .MinimumLevel.Is(LogEventLevel.Verbose) - .MinimumLevel.Override("Microsoft", LogEventLevel.Error) - .MinimumLevel.Override("System", LogEventLevel.Error) - .WriteTo.Console() - .CreateBootstrapLogger(); - } - - private static IConfigurationRoot BuildConfiguration(string[] args) - { - var environmentAndCommandLineConfiguration = new ConfigurationBuilder() - .AddEnvironmentVariablesAndCommandLine(args) - .Build(); - - var environmentName = environmentAndCommandLineConfiguration.EnvironmentName(); - - var builder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", true, false) - .AddJsonFile($"appsettings.{environmentName}.json", true, false) - .AddCustomSimulatorConfigFileIfSpecified(environmentAndCommandLineConfiguration) - .AddEnvironmentVariablesAndCommandLine(args) - .AddInMemoryCollection( - new Dictionary - { - ["AEGS_Serilog__Using__0"] = "Serilog.Sinks.Console", - ["AEGS_Serilog__Using__1"] = "Serilog.Sinks.File", - ["AEGS_Serilog__Using__2"] = "Serilog.Sinks.Seq" - }); - - return builder.Build(); - } - - private static WebApplicationBuilder ConfigureWebHost(string[] args, IConfiguration configuration) - { - var builder = WebApplication.CreateBuilder(args); - - builder.Host.ConfigureServices(services => - { - services.AddSimulatorSettings(configuration); - services.AddMediatR(Assembly.GetExecutingAssembly()); - services.AddHttpClient(); - - services.AddScoped(); - services.AddSingleton(); - - services.AddControllers(options => { options.EnableEndpointRouting = false; }) - .AddJsonOptions(options => { options.JsonSerializerOptions.WriteIndented = true; }); - - services.AddApiVersioning(config => - { - config.DefaultApiVersion = new ApiVersion(DateTime.Parse(Constants.SupportedApiVersion, new ApiVersionFormatProvider())); - config.AssumeDefaultVersionWhenUnspecified = true; - config.ReportApiVersions = true; - }); - }); - - builder.Host.ConfigureLogging(loggingBuilder => { loggingBuilder.ClearProviders(); }); - builder.Host.UseSerilog((context, loggerConfiguration) => - { - var hasAtLeastOneLogSinkBeenConfigured = context.Configuration.GetSection("Serilog:WriteTo").GetChildren().ToArray().Any(); - - loggerConfiguration - .Enrich.FromLogContext() - .Enrich.WithProperty("MachineName", Environment.MachineName) - .Enrich.WithProperty("Environment", context.Configuration.EnvironmentName()) - .Enrich.WithProperty("Application", nameof(AzureEventGridSimulator)) - .Enrich.WithProperty("Version", Assembly.GetExecutingAssembly().GetName().Version) - // The sensible defaults - .MinimumLevel.Is(LogEventLevel.Information) - .MinimumLevel.Override("Microsoft", LogEventLevel.Error) - .MinimumLevel.Override("System", LogEventLevel.Error) - // Override defaults from settings if any - .ReadFrom.Configuration(context.Configuration, "Serilog") - .WriteTo.Conditional(_ => !hasAtLeastOneLogSinkBeenConfigured, sinkConfiguration => sinkConfiguration.Console()); - }); - - builder.WebHost - .ConfigureAppConfiguration((_, configBuilder) => - { - //configBuilder.Sources.Clear(); - configBuilder.AddConfiguration(configuration); - }) - .UseKestrel(options => - { - Log.Verbose(((IConfigurationRoot)configuration).GetDebugView().Normalize()); - - options.ConfigureSimulatorCertificate(); - - foreach (var topics in options.ApplicationServices.EnabledTopics()) - { - options.Listen(IPAddress.Any, - topics.Port, - listenOptions => listenOptions - .UseHttps()); - } - }); - - return builder; - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using AzureEventGridSimulator.Domain; +using AzureEventGridSimulator.Domain.Commands; +using AzureEventGridSimulator.Infrastructure; +using AzureEventGridSimulator.Infrastructure.Extensions; +using AzureEventGridSimulator.Infrastructure.Middleware; +using AzureEventGridSimulator.Infrastructure.Settings; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Versioning; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Events; +using ILogger = Serilog.ILogger; + +[assembly: InternalsVisibleTo("AzureEventGridSimulator.Tests")] + +namespace AzureEventGridSimulator; + +public class Program +{ + public static async Task Main(string[] args) + { + try + { + // Build it and fire it up + var app = CreateWebHostBuilder(args) + .Build(); + + app.UseSerilogRequestLogging(options => { options.GetLevel = (_, _, _) => LogEventLevel.Debug; }); + app.UseEventGridMiddleware(); + app.UseRouting(); + app.UseEndpoints(e => { e.MapControllers(); }); + + await StartSimulator(app); + } + catch (Exception ex) + { + Log.Fatal(ex, "Failed to start the Azure Event Grid Simulator"); + } + finally + { + Log.CloseAndFlush(); + } + } + + public static async Task StartSimulator(WebApplication host, CancellationToken token = default) + { + try + { + await host.StartAsync(token) + .ContinueWith(_ => OnApplicationStarted(host, host.Lifetime), token) + .ConfigureAwait(false); + + await host.WaitForShutdownAsync(token).ConfigureAwait(false); + } + finally + { + await host.DisposeAsync().ConfigureAwait(false); + } + } + + private static async Task OnApplicationStarted(IApplicationBuilder app, IHostApplicationLifetime lifetime) + { + try + { + Log.Verbose("Started"); + + var simulatorSettings = app.ApplicationServices.GetService(); + + if (simulatorSettings is null) + { + Log.Fatal("Settings are not found. The application will now exit"); + lifetime.StopApplication(); + return; + } + + if (!simulatorSettings.Topics.Any()) + { + Log.Fatal("There are no configured topics. The application will now exit"); + lifetime.StopApplication(); + return; + } + + if (simulatorSettings.Topics.All(o => o.Disabled)) + { + Log.Fatal("All of the configured topics are disabled. The application will now exit"); + lifetime.StopApplication(); + return; + } + + var mediator = app.ApplicationServices.GetService(); + + if (mediator is null) + { + Log.Fatal("Required component was not found. The application will now exit"); + lifetime.StopApplication(); + return; + } + + await mediator.Send(new ValidateAllSubscriptionsCommand()); + + Log.Information("It's alive !"); + } + catch (Exception e) + { + Log.Fatal(e, "It died !"); + lifetime.StopApplication(); + } + } + + private static WebApplicationBuilder CreateWebHostBuilder(string[] args) + { + // Set up basic Console logger we can use to log to until we've finished building everything + Log.Logger = CreateBasicConsoleLogger(); + + // First thing's first. Build the configuration. + var configuration = BuildConfiguration(args); + + // Configure the web host builder + return ConfigureWebHost(args, configuration); + } + + private static ILogger CreateBasicConsoleLogger() + { + return new LoggerConfiguration() + .MinimumLevel.Is(LogEventLevel.Verbose) + .MinimumLevel.Override("Microsoft", LogEventLevel.Error) + .MinimumLevel.Override("System", LogEventLevel.Error) + .WriteTo.Console() + .CreateBootstrapLogger(); + } + + private static IConfigurationRoot BuildConfiguration(string[] args) + { + var environmentAndCommandLineConfiguration = new ConfigurationBuilder() + .AddEnvironmentVariablesAndCommandLine(args) + .Build(); + + var environmentName = environmentAndCommandLineConfiguration.EnvironmentName(); + + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{environmentName}.json", true, false) + .AddCustomSimulatorConfigFileIfSpecified(environmentAndCommandLineConfiguration) + .AddEnvironmentVariablesAndCommandLine(args) + .AddInMemoryCollection( + new Dictionary + { + ["AEGS_Serilog__Using__0"] = "Serilog.Sinks.Console", + ["AEGS_Serilog__Using__1"] = "Serilog.Sinks.File", + ["AEGS_Serilog__Using__2"] = "Serilog.Sinks.Seq" + }); + + return builder.Build(); + } + + private static WebApplicationBuilder ConfigureWebHost(string[] args, IConfiguration configuration) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Host.ConfigureServices(services => + { + services.AddSimulatorSettings(configuration); + services.AddMediatR(Assembly.GetExecutingAssembly()); + services.AddHttpClient(); + + services.AddScoped(); + services.AddSingleton(); + + services.AddControllers(options => { options.EnableEndpointRouting = false; }) + .AddJsonOptions(options => { options.JsonSerializerOptions.WriteIndented = true; }); + + services.AddApiVersioning(config => + { + config.DefaultApiVersion = new ApiVersion(DateTime.Parse(Constants.SupportedApiVersion, new ApiVersionFormatProvider())); + config.AssumeDefaultVersionWhenUnspecified = true; + config.ReportApiVersions = true; + }); + }); + + builder.Host.ConfigureLogging(loggingBuilder => { loggingBuilder.ClearProviders(); }); + builder.Host.UseSerilog((context, loggerConfiguration) => + { + var hasAtLeastOneLogSinkBeenConfigured = context.Configuration.GetSection("Serilog:WriteTo").GetChildren().ToArray().Any(); + + loggerConfiguration + .Enrich.FromLogContext() + .Enrich.WithProperty("MachineName", Environment.MachineName) + .Enrich.WithProperty("Environment", context.Configuration.EnvironmentName()) + .Enrich.WithProperty("Application", nameof(AzureEventGridSimulator)) + .Enrich.WithProperty("Version", Assembly.GetExecutingAssembly().GetName().Version) + // The sensible defaults + .MinimumLevel.Is(LogEventLevel.Information) + .MinimumLevel.Override("Microsoft", LogEventLevel.Error) + .MinimumLevel.Override("System", LogEventLevel.Error) + // Override defaults from settings if any + .ReadFrom.Configuration(context.Configuration, "Serilog") + .WriteTo.Conditional(_ => !hasAtLeastOneLogSinkBeenConfigured, sinkConfiguration => sinkConfiguration.Console()); + }); + + builder.WebHost + .ConfigureAppConfiguration((_, configBuilder) => + { + //configBuilder.Sources.Clear(); + configBuilder.AddConfiguration(configuration); + }) + .UseKestrel(options => + { + Log.Verbose(((IConfigurationRoot)configuration).GetDebugView().Normalize()); + + options.ConfigureSimulatorCertificate(); + + foreach (var topics in options.ApplicationServices.EnabledTopics()) + { + options.Listen(IPAddress.Any, + topics.Port, + listenOptions => listenOptions + .UseHttps()); + } + }); + + return builder; + } +} From b0ea61bb075cb9925666ce93a7c16fa11cc6e225 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 12 Sep 2022 12:22:11 +0100 Subject: [PATCH 8/9] setup test for cloudevent --- .../AzureMessagingEventGridTest.cs | 39 +++++++++++++++++++ .../Controllers/NotificationController.cs | 1 - .../Domain/Entities/CloudEventGridEvent.cs | 2 +- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/AzureEventGridSimulator.Tests/ActualSimulatorTests/AzureMessagingEventGridTest.cs b/src/AzureEventGridSimulator.Tests/ActualSimulatorTests/AzureMessagingEventGridTest.cs index 4692810..17d972b 100644 --- a/src/AzureEventGridSimulator.Tests/ActualSimulatorTests/AzureMessagingEventGridTest.cs +++ b/src/AzureEventGridSimulator.Tests/ActualSimulatorTests/AzureMessagingEventGridTest.cs @@ -1,11 +1,14 @@ using System; using System.Net; +using System.Text; using System.Threading.Tasks; using Azure; using Azure.Core; +using Azure.Messaging; using Azure.Messaging.EventGrid; using Shouldly; using Xunit; +using EventGridEvent = Azure.Messaging.EventGrid.EventGridEvent; namespace AzureEventGridSimulator.Tests.ActualSimulatorTests; @@ -40,6 +43,42 @@ public async Task GivenValidEvent_WhenUriContainsNonStandardPort_ThenItShouldBeA response.Status.ShouldBe((int)HttpStatusCode.OK); } + + [Fact] + public async Task GivenValidCloudEvent_WhenUriContainsNonStandardPort_ThenItShouldBeAccepted() + { + var client = new EventGridPublisherClient( + new Uri("https://localhost:60101/api/events/cloudevent"), + new AzureKeyCredential("TheLocal+DevelopmentKey="), + new EventGridPublisherClientOptions + { Retry = { Mode = RetryMode.Fixed, MaxRetries = 0, NetworkTimeout = TimeSpan.FromSeconds(5) } }); + + + + //var cloudEvent = new CloudEventGridEvent() + //{ + // Data_Base64 = "", + // Id = "1232", + // Source = "https://awesomesource.com/somestuff", + // Type = "The.Event.Type", + // Time = DateTimeOffset.UtcNow, + // DataSchema = "https://awesomeschema.com/someuri", + // DataContentType = "application/json", + // Subject = "/the/subject", + //}; + + + + var data = new BinaryData(Encoding.UTF8.GetBytes("##This is treated as binary data##")); + + var myEvent = new CloudEvent("https://awesomesource.com/somestuff", "The.Event.Type", data, "application/cloudevents-batch+json"); + + var response = await client.SendEventAsync(myEvent); + + response.Status.ShouldBe((int)HttpStatusCode.OK); + } + + [Fact] public async Task GivenValidEvents_WhenUriContainsNonStandardPort_TheyShouldBeAccepted() { diff --git a/src/AzureEventGridSimulator/Controllers/NotificationController.cs b/src/AzureEventGridSimulator/Controllers/NotificationController.cs index e0ba34d..201aa76 100644 --- a/src/AzureEventGridSimulator/Controllers/NotificationController.cs +++ b/src/AzureEventGridSimulator/Controllers/NotificationController.cs @@ -43,7 +43,6 @@ public async Task Post() [HttpPost] public async Task PostCloudEvent() { - var topicSettingsForCurrentRequestPort = _simulatorSettings.Topics.First(t => t.Port == HttpContext.Request.Host.Port); var eventsFromCurrentRequestBody = JsonConvert.DeserializeObject(await HttpContext.RequestBody()); diff --git a/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs b/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs index 8bcdd80..470004d 100644 --- a/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs +++ b/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs @@ -33,7 +33,7 @@ public class CloudEventGridEvent [JsonProperty("subject")] public string Subject { get; set; } - public void Validate() + public void Validate() { if (string.IsNullOrWhiteSpace(Id)) { From b665a4461df0b2bbe958c2bbefc37ae37d630516 Mon Sep 17 00:00:00 2001 From: DanielBarrettHE Date: Tue, 13 Sep 2022 09:37:34 +0100 Subject: [PATCH 9/9] Unit / integration tests --- .../AzureMessagingEventGridTest.cs | 45 ++++++++++--------- .../IntegrationTests/BasicTests.cs | 37 +++++++++++++++ .../Domain/Entities/CloudEventGridEvent.cs | 11 ----- 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/src/AzureEventGridSimulator.Tests/ActualSimulatorTests/AzureMessagingEventGridTest.cs b/src/AzureEventGridSimulator.Tests/ActualSimulatorTests/AzureMessagingEventGridTest.cs index 17d972b..00d42fd 100644 --- a/src/AzureEventGridSimulator.Tests/ActualSimulatorTests/AzureMessagingEventGridTest.cs +++ b/src/AzureEventGridSimulator.Tests/ActualSimulatorTests/AzureMessagingEventGridTest.cs @@ -22,7 +22,9 @@ namespace AzureEventGridSimulator.Tests.ActualSimulatorTests; public class AzureMessagingEventGridTest { // ReSharper disable once NotAccessedField.Local - private readonly ActualSimulatorFixture _actualSimulatorFixture; + private readonly ActualSimulatorFixture _actualSimulatorFixture; + + private BinaryData _data = new BinaryData(Encoding.UTF8.GetBytes("##This is treated as binary data##")); public AzureMessagingEventGridTest(ActualSimulatorFixture actualSimulatorFixture) { @@ -52,33 +54,14 @@ public async Task GivenValidCloudEvent_WhenUriContainsNonStandardPort_ThenItShou new AzureKeyCredential("TheLocal+DevelopmentKey="), new EventGridPublisherClientOptions { Retry = { Mode = RetryMode.Fixed, MaxRetries = 0, NetworkTimeout = TimeSpan.FromSeconds(5) } }); - - - - //var cloudEvent = new CloudEventGridEvent() - //{ - // Data_Base64 = "", - // Id = "1232", - // Source = "https://awesomesource.com/somestuff", - // Type = "The.Event.Type", - // Time = DateTimeOffset.UtcNow, - // DataSchema = "https://awesomeschema.com/someuri", - // DataContentType = "application/json", - // Subject = "/the/subject", - //}; - - - var data = new BinaryData(Encoding.UTF8.GetBytes("##This is treated as binary data##")); - - var myEvent = new CloudEvent("https://awesomesource.com/somestuff", "The.Event.Type", data, "application/cloudevents-batch+json"); + var myEvent = new CloudEvent("https://awesomesource.com/somestuff", "The.Event.Type", _data, "application/json"); var response = await client.SendEventAsync(myEvent); response.Status.ShouldBe((int)HttpStatusCode.OK); } - [Fact] public async Task GivenValidEvents_WhenUriContainsNonStandardPort_TheyShouldBeAccepted() { @@ -99,6 +82,26 @@ public async Task GivenValidEvents_WhenUriContainsNonStandardPort_TheyShouldBeAc response.Status.ShouldBe((int)HttpStatusCode.OK); } + [Fact] + public async Task GivenValidCloudEvents_WhenUriContainsNonStandardPort_TheyShouldBeAccepted() + { + var client = new EventGridPublisherClient( + new Uri("https://localhost:60101/api/events/cloudevent"), + new AzureKeyCredential("TheLocal+DevelopmentKey="), + new EventGridPublisherClientOptions + { Retry = { Mode = RetryMode.Fixed, MaxRetries = 0, NetworkTimeout = TimeSpan.FromSeconds(5) } }); + + var events = new[] + { + new CloudEvent("https://awesomesource.com/somestuff1", "The.Event.Type1", _data, "application/json"), + new CloudEvent("https://awesomesource.com/somestuff2", "The.Event.Type2", _data, "application/json") + }; + + var response = await client.SendEventsAsync(events); + + response.Status.ShouldBe((int)HttpStatusCode.OK); + } + [Fact] public async Task GivenValidEvent_WhenUriContainsNonExistentPort_ThenItShouldNotBeAccepted() { diff --git a/src/AzureEventGridSimulator.Tests/IntegrationTests/BasicTests.cs b/src/AzureEventGridSimulator.Tests/IntegrationTests/BasicTests.cs index 4e99f12..56f8e1f 100644 --- a/src/AzureEventGridSimulator.Tests/IntegrationTests/BasicTests.cs +++ b/src/AzureEventGridSimulator.Tests/IntegrationTests/BasicTests.cs @@ -51,4 +51,41 @@ public async Task GivenAValidEvent_WhenPublished_ThenItShouldBeAccepted() response.EnsureSuccessStatusCode(); response.StatusCode.ShouldBe(HttpStatusCode.OK); } + + [Fact] + public async Task GivenAValidCloudEvent_WhenPublished_ThenItShouldBeAccepted() + { + // Arrange + var client = _factory.CreateClient(new WebApplicationFactoryClientOptions + { + BaseAddress = new Uri("https://localhost:60101") + }); + + client.DefaultRequestHeaders.Add(Constants.AegSasKeyHeader, "TheLocal+DevelopmentKey="); + client.DefaultRequestHeaders.Add(Constants.AegEventTypeHeader, Constants.NotificationEventType); + + var data = new BinaryData(Encoding.UTF8.GetBytes("##This is treated as binary data##")); + + var testEvent = new Domain.Entities.CloudEventGridEvent() + { + Data_Base64 = Convert.ToBase64String(data), + Id = "1232", + Source = "https://awesomesource.com/somestuff", + Type = "The.Event.Type", + Time = DateTimeOffset.UtcNow, + DataSchema = "https://awesomeschema.com/someuri", + DataContentType = "application/json", + Subject = "/the/subject", + }; + + var json = JsonConvert.SerializeObject(new[] { testEvent }, Formatting.Indented); + + // Act + var jsonContent = new StringContent(json, Encoding.UTF8, "application/json"); + var response = await client.PostAsync("/api/events/cloudevent", jsonContent); + + // Assert + response.EnsureSuccessStatusCode(); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } } diff --git a/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs b/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs index 470004d..de2d05e 100644 --- a/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs +++ b/src/AzureEventGridSimulator/Domain/Entities/CloudEventGridEvent.cs @@ -55,21 +55,10 @@ public void Validate() throw new InvalidOperationException($"Required property '{nameof(Data_Base64)}' was not set."); } - if (string.IsNullOrWhiteSpace(DataSchema)) - { - throw new InvalidOperationException($"Required property '{nameof(DataSchema)}' was not set."); - } - if (string.IsNullOrWhiteSpace(DataContentType)) { throw new InvalidOperationException($"Required property '{nameof(DataContentType)}' was not set."); } - - if (string.IsNullOrWhiteSpace(Subject)) - { - throw new InvalidOperationException($"Required property '{nameof(Subject)}' was not set."); - } - } }