// [Route("Subscribe")] public async Task <AcknowledgeAction> SubscribeAsync(SubscribeAction action) { var connectionId = CurrentRequest.RequestContext.ConnectionId; LogInfo($"Subscribe request from: {connectionId}"); // validate request if (string.IsNullOrEmpty(action.Rule)) { return(action.AcknowledgeError("missing or invalid rule name")); } // retrieve connection record var connection = await _dataTable.GetConnectionRecordAsync(connectionId); if (connection == null) { LogInfo("Connection was removed"); return(action.AcknowledgeError("connection gone")); } if (connection.State == ConnectionState.Failed) { LogInfo("Connection is in failed state"); throw Abort(action.AcknowledgeError("connection reset required")); } if (connection.SubscriptionArn == null) { LogInfo("Client has not announced itself"); return(action.AcknowledgeError("client is unannounced")); } if (connection.State != ConnectionState.Open) { LogInfo("Connection is not open (state: {0})", connection.State); throw Abort(action.AcknowledgeError("action not allowed")); } // validate pattern var validPattern = false; try { validPattern = EventPatternMatcher.IsValid(JObject.Parse(action.Pattern)); } catch { // nothing to do } if (!validPattern) { return(action.AcknowledgeError("invalid pattern")); } // create or update event rule await _dataTable.CreateOrUpdateRuleRecordAsync(new RuleRecord { Rule = action.Rule, Pattern = action.Pattern, ConnectionId = connection.ConnectionId }); return(action.AcknowledgeOk()); }
public void Pattern_with_null_serialized_is_valid() { // arrange var pattern = JObject.Parse("{\"source\":[\"Sample.BlazorEventsSample:1.0-DEV@stevebv7-lambdasharp-cor-deploymentbucketresource-9h53iqcat7uj::MyBlazorApp\"],\"detail-type\":[\"Sample.BlazorEventsSample.MyBlazorApp.Shared.TodoItem\"],\"resources\":[\"lambdasharp:tier:SteveBv7\"],\"detail\":null}"); // act var isValid = EventPatternMatcher.IsValid(pattern); // assert isValid.Should().BeTrue(); }
public void Empty_pattern_is_not_valid() { // arrange var pattern = JObject.Parse(@"{}"); // act var isValid = EventPatternMatcher.IsValid(pattern); // assert isValid.Should().BeFalse(); }
public void Pattern_with_invalid_filter_is_not_valid() { // arrange var pattern = JObject.Parse(@"{ ""Foo"": [ { ""Bar"": 42 } ] }"); // act var isValid = EventPatternMatcher.IsValid(pattern); // assert isValid.Should().BeFalse(); }
public void Pattern_with_exists_null_is_not_valid() { // arrange var pattern = JObject.Parse(@"{ ""Foo"": [ { ""exists"": null } ] }"); // act var isValid = EventPatternMatcher.IsValid(pattern); // assert isValid.Should().BeFalse(); }
public void Pattern_with_cidr_bad_prefix_is_not_valid() { // arrange var pattern = JObject.Parse(@"{ ""Foo"": [ { ""cidr"": ""1.1.1.1/42"" } ] }"); // act var isValid = EventPatternMatcher.IsValid(pattern); // assert isValid.Should().BeFalse(); }
public void Pattern_with_numeric_two_operations_is_valid() { // arrange var pattern = JObject.Parse(@"{ ""Foo"": [ { ""numeric"": [ "">"", 42, ""<"", 404 ] } ] }"); // act var isValid = EventPatternMatcher.IsValid(pattern); // assert isValid.Should().BeTrue(); }
public void Pattern_with_anything_but_content_filter_is_valid() { // arrange var pattern = JObject.Parse(@"{ ""Foo"": [ { ""anything-but"": { ""prefix"": ""Bar"" } } ] }"); // act var isValid = EventPatternMatcher.IsValid(pattern); // assert isValid.Should().BeTrue(); }
public void Pattern_with_anything_but_numeric_is_valid() { // arrange var pattern = JObject.Parse(@"{ ""Foo"": [ { ""anything-but"": 42 } ] }"); // act var isValid = EventPatternMatcher.IsValid(pattern); // assert isValid.Should().BeTrue(); }
public void Pattern_with_prefix_list_is_not_valid() { // arrange var pattern = JObject.Parse(@"{ ""Foo"": [ { ""prefix"": [ ""Bar"" ] } ] }"); // act var isValid = EventPatternMatcher.IsValid(pattern); // assert isValid.Should().BeFalse(); }
public void Pattern_with_null_is_valid() { // arrange var pattern = JObject.Parse(@"{ ""Foo"": [ null ] }"); // act var isValid = EventPatternMatcher.IsValid(pattern); // assert isValid.Should().BeTrue(); }
public void Pattern_with_nested_list_is_not_valid() { // arrange var pattern = JObject.Parse(@"{ ""Foo"": [ [ 42 ] ] }"); // act var isValid = EventPatternMatcher.IsValid(pattern); // assert isValid.Should().BeFalse(); }
public void Empty_event_is_not_matched() { // arrange var evt = JObject.Parse(@"{}"); var pattern = JObject.Parse(@"{ ""Foo"": [ ""Bar"" ] }"); // act var isMatch = EventPatternMatcher.IsMatch(evt, pattern); // assert isMatch.Should().BeFalse(); }
public void Event_with_not_exists_is_matched() { // arrange var evt = JObject.Parse(@"{}"); var pattern = JObject.Parse(@"{ ""Foo"": [ { ""exists"": false } ] }"); // act var isMatch = EventPatternMatcher.IsMatch(evt, pattern); // assert isMatch.Should().BeTrue(); }
public void Event_with_prefix_is_not_matched() { // arrange var evt = JObject.Parse(@"{ ""Foo"": ""Bar"" }"); var pattern = JObject.Parse(@"{ ""Foo"": [ { ""prefix"": ""F"" } ] }"); // act var isMatch = EventPatternMatcher.IsMatch(evt, pattern); // assert isMatch.Should().BeFalse(); }
public void Event_with_list_is_matched() { // arrange var evt = JObject.Parse(@"{ ""Foo"": [ ""Bar"" ] }"); var pattern = JObject.Parse(@"{ ""Foo"": [ ""Bar"" ] }"); // act var isMatch = EventPatternMatcher.IsMatch(evt, pattern); // assert isMatch.Should().BeTrue(); }
public void Event_with_cidr_mismatch_is_not_matched() { // arrange var evt = JObject.Parse(@"{ ""Foo"": ""Bar"" }"); var pattern = JObject.Parse(@"{ ""Foo"": [ { ""cidr"": ""192.168.1.1/24"" } ] }"); // act var isMatch = EventPatternMatcher.IsMatch(evt, pattern); // assert isMatch.Should().BeFalse(); }
public void Event_with_numeric_one_operation_type_mismatch_is_not_matched() { // arrange var evt = JObject.Parse(@"{ ""Foo"": ""Bar"" }"); var pattern = JObject.Parse(@"{ ""Foo"": [ { ""numeric"": [ "">="", 40 ] } ] }"); // act var isMatch = EventPatternMatcher.IsMatch(evt, pattern); // assert isMatch.Should().BeFalse(); }
public void Event_with_numeric_two_operation_is_matched() { // arrange var evt = JObject.Parse(@"{ ""Foo"": 42 }"); var pattern = JObject.Parse(@"{ ""Foo"": [ { ""numeric"": [ "">="", 40, ""<"", 404 ] } ] }"); // act var isMatch = EventPatternMatcher.IsMatch(evt, pattern); // assert isMatch.Should().BeTrue(); }
public void Event_with_anything_but_is_matched() { // arrange var evt = JObject.Parse(@"{ ""Foo"": ""Bar"" }"); var pattern = JObject.Parse(@"{ ""Foo"": [ { ""anything-but"": { ""prefix"": ""F"" } } ] }"); // act var isMatch = EventPatternMatcher.IsMatch(evt, pattern); // assert isMatch.Should().BeTrue(); }
public void Event_with_nested_prefix_is_matched() { // arrange var evt = JObject.Parse(@"{ ""Foo"": { ""Bar"": ""ABC"" } }"); var pattern = JObject.Parse(@"{ ""Foo"": { ""Bar"": [ { ""prefix"": ""A"" } ] } }"); // act var isMatch = EventPatternMatcher.IsMatch(evt, pattern); // assert isMatch.Should().BeTrue(); }
public override async Task <APIGatewayHttpApiV2ProxyResponse> ProcessMessageAsync(APIGatewayHttpApiV2ProxyRequest request) { LogInfo($"Message received at {request.RequestContext.Http.Method}:{request.RawPath}?{request.RawQueryString}"); // validate invocation method if (request.RequestContext.Http.Method != "POST") { LogInfo("Unsupported request method {0}", request.RequestContext.Http.Method); return(BadRequest()); } // validate request token if ( !request.QueryStringParameters.TryGetValue("token", out var token) || (token != _httpApiToken) ) { LogInfo("Missing or invalid request token"); return(BadRequest()); } // validate request websocket if ( !request.QueryStringParameters.TryGetValue("ws", out var connectionId) || string.IsNullOrEmpty(connectionId) ) { LogInfo("Invalid websocket connection id"); return(BadRequest()); } // validate request id if ( !request.QueryStringParameters.TryGetValue("rid", out var requestId) || string.IsNullOrEmpty(requestId) ) { LogInfo("Invalid request id"); return(BadRequest()); } // check if request is a subscription confirmation var topicSubscription = LambdaSerializer.Deserialize <TopicSubscriptionPayload>(request.Body); if (topicSubscription.Type == "SubscriptionConfirmation") { // confirm it's for the expected topic ARN if (topicSubscription.TopicArn != _eventTopicArn) { LogWarn("Wrong Topic ARN for subscription confirmation (Expected: {0}, Received: {1})", _eventTopicArn, topicSubscription.TopicArn); return(BadRequest()); } // confirm subscription await HttpClient.GetAsync(topicSubscription.SubscribeURL); // send welcome action to websocket connection await SendMessageToConnection(new AcknowledgeAction { RequestId = requestId, Status = "Ok" }, connectionId); return(Success("Confirmed")); } // validate SNS message var snsMessage = LambdaSerializer.Deserialize <SNSEvent.SNSMessage>(request.Body); if (snsMessage.Message == null) { LogWarn("Invalid SNS message received: {0}", request.Body); return(BadRequest()); } // validate CloudWatch event var cloudWatchEvent = LambdaSerializer.Deserialize <CloudWatchEventPayload>(snsMessage.Message); if ( (cloudWatchEvent.Source == null) || (cloudWatchEvent.DetailType == null) || (cloudWatchEvent.Resources == null) ) { LogInfo("Invalid CloudWatch event received: {0}", snsMessage.Message); return(BadRequest()); } // check if the keep-alive event was received if ( (cloudWatchEvent.Source == "aws.events") && (cloudWatchEvent.DetailType == "Scheduled Event") && (cloudWatchEvent.Resources.Count == 1) && (cloudWatchEvent.Resources[0] == _keepAliveRuleArn) ) { // send keep-alive action to websocket connection await SendMessageToConnection(new KeepAliveAction(), connectionId); return(Success("Ok")); } // determine what rules are matching JObject evt; try { evt = JObject.Parse(snsMessage.Message); } catch (Exception e) { LogError(e, "invalid message"); return(BadRequest()); } var rules = await _dataTable.GetConnectionRulesAsync(connectionId); var matchedRules = rules .Where(rule => { try { var pattern = JObject.Parse(rule.Pattern); return(EventPatternMatcher.IsMatch(evt, pattern)); } catch (Exception e) { LogError(e, "invalid event pattern: {0}", rule.Pattern); return(false); } }).Select(rule => rule.Rule) .ToList(); if (matchedRules.Any()) { await SendMessageToConnection( new EventAction { Rules = matchedRules, Source = cloudWatchEvent.Source, Type = cloudWatchEvent.DetailType, Event = snsMessage.Message }, connectionId ); } return(Success("Ok")); // local functions APIGatewayHttpApiV2ProxyResponse Success(string message) => new APIGatewayHttpApiV2ProxyResponse { Body = message, Headers = new Dictionary <string, string> { ["Content-Type"] = "text/plain" }, StatusCode = (int)HttpStatusCode.OK }; APIGatewayHttpApiV2ProxyResponse BadRequest() => new APIGatewayHttpApiV2ProxyResponse { Body = "Bad Request", Headers = new Dictionary <string, string> { ["Content-Type"] = "text/plain" }, StatusCode = (int)HttpStatusCode.BadRequest }; }
private async Task <APIGatewayHttpApiV2ProxyResponse> DispatchEvent(string connectionId, string message) { // validate EventBridge event var eventBridgeEvent = LambdaSerializer.Deserialize <EventBridgeventPayload>(message); if ( (eventBridgeEvent.Source == null) || (eventBridgeEvent.DetailType == null) || (eventBridgeEvent.Resources == null) ) { LogInfo("Invalid EventBridge event received: {0}", message); return(BadRequestResponse()); } // check if the keep-alive event was received if ( (eventBridgeEvent.Source == "aws.events") && (eventBridgeEvent.DetailType == "Scheduled Event") && (eventBridgeEvent.Resources?.Count == 1) && (eventBridgeEvent.Resources[0] == _keepAliveRuleArn) ) { // retrieve connection record var connection = await _dataTable.GetConnectionRecordAsync(connectionId); if (connection == null) { return(SuccessResponse("Gone")); } if (connection.State != ConnectionState.Open) { return(SuccessResponse("Ignored")); } // send keep-alive action to websocket connection LogInfo("KeepAlive tick"); await SendActionToConnection(new KeepAliveAction(), connectionId); return(SuccessResponse("Ok")); } // determine what rules are matching the event JObject evt; try { evt = JObject.Parse(message); } catch (Exception e) { LogError(e, "invalid message"); return(BadRequestResponse()); } var rules = await _dataTable.GetAllRuleRecordAsync(connectionId); var matchedRules = rules .Where(rule => { try { var pattern = JObject.Parse(rule.Pattern); return(EventPatternMatcher.IsMatch(evt, pattern)); } catch (Exception e) { LogError(e, "invalid event pattern: {0}", rule.Pattern); return(false); } }).Select(rule => rule.Rule) .ToList(); if (matchedRules.Any()) { LogInfo($"Event matched {matchedRules.Count():N0} rules: {string.Join(", ", matchedRules)}"); await SendActionToConnection( new EventAction { Rules = matchedRules, Source = eventBridgeEvent.Source, Type = eventBridgeEvent.DetailType, Event = message }, connectionId ); } else { LogInfo("Event matched no rules"); } return(SuccessResponse("Ok")); }
// [Route("Subscribe")] public async Task <AcknowledgeAction> SubscribeAsync(SubscribeAction action) { var connectionId = CurrentRequest.RequestContext.ConnectionId; LogInfo($"Subscribe request from: {connectionId}"); // validate request if (string.IsNullOrEmpty(action.Rule)) { return(new AcknowledgeAction { RequestId = action.RequestId, Status = "Error", Message = "Missing or invalid rule name" }); } // retrieve websocket connection record var connection = await _dataTable.GetConnectionAsync(connectionId); if (connection == null) { LogInfo("Connection was removed"); return(new AcknowledgeAction { RequestId = action.RequestId, Rule = action.Rule, Status = "Error", Message = "Connection gone" }); } // validate pattern var validPattern = false; try { validPattern = EventPatternMatcher.IsValid(JObject.Parse(action.Pattern)); } catch { // nothing to do } if (!validPattern) { return(new AcknowledgeAction { RequestId = action.RequestId, Rule = action.Rule, Status = "Error", Message = "Invalid pattern" }); } // create or update event rule await _dataTable.CreateOrUpdateRuleAsync(new RuleRecord { Rule = action.Rule, Pattern = action.Pattern, ConnectionId = connection.ConnectionId }); return(new AcknowledgeAction { RequestId = action.RequestId, Rule = action.Rule, Status = "Ok" }); }