private async Task <IActionResult> HandleIntentVerificationAsync(WebSubSubscription subscription, IQueryCollection requestQuery, IServiceProvider requestServices) { IActionResult intentVerificationResult = new NotFoundResult(); if (subscription != null) { IWebSubSubscriptionsStore subscriptionsStore = requestServices.GetRequiredService <IWebSubSubscriptionsStore>(); IWebSubSubscriptionsService subscriptionsService = requestServices.GetService <IWebSubSubscriptionsService>(); switch (requestQuery[WebSubConstants.MODE_QUERY_PARAMETER_NAME]) { case WebSubConstants.MODE_DENIED: intentVerificationResult = await HandleSubscribeIntentDenyAsync(subscription, subscriptionsStore, subscriptionsService, requestQuery); break; case WebSubConstants.MODE_SUBSCRIBE: intentVerificationResult = await HandleSubscribeIntentVerificationAsync(subscription, subscriptionsStore, subscriptionsService, requestQuery); break; case WebSubConstants.MODE_UNSUBSCRIBE: intentVerificationResult = await HandleUnsubscribeIntentVerificationAsync(subscription, subscriptionsStore, subscriptionsService, requestQuery); break; default: intentVerificationResult = HandleBadRequest($"A '{WebSubConstants.ReceiverName}' WebHook intent verification request contains unknown '{WebSubConstants.MODE_QUERY_PARAMETER_NAME}' query parameter value."); break; } } return(intentVerificationResult); }
private static Task <HttpResponseMessage> HandleIntentVerificationAsync(WebSubSubscription subscription, HttpRequestMessage request) { Dictionary <string, string> requestQuery = request.GetQueryNameValuePairs().ToDictionary(nameValuePair => nameValuePair.Key, nameValuePair => nameValuePair.Value, StringComparer.OrdinalIgnoreCase); if (requestQuery.TryGetValue(MODE_QUERY_PARAMETER_NAME, out string mode)) { switch (mode) { case MODE_DENIED: return(HandleSubscribeIntentDenyAsync(request, requestQuery, subscription)); case MODE_SUBSCRIBE: return(HandleSubscribeIntentVerificationAsync(request, requestQuery, subscription)); case MODE_UNSUBSCRIBE: return(HandleUnsubscribeIntentVerificationAsync(request, requestQuery, subscription)); default: return(Task.FromResult(HandleBadRequest(request, $"A '{ReceiverName}' WebHook intent verification request contains unknown '{MODE_QUERY_PARAMETER_NAME}' query parameter value."))); } } else { return(Task.FromResult(HandleMissingIntentVerificationParameter(request, MODE_QUERY_PARAMETER_NAME))); } }
private async Task <bool> VerifyUnsubscribeIntentAsync(WebSubSubscription subscription, IWebSubSubscriptionsStore subscriptionsStore, IWebSubSubscriptionsService subscriptionsService, string topic) { bool verified = false; if (subscription.State == WebSubSubscriptionState.UnsubscribeRequested) { if (subscription.TopicUrl != topic) { if (subscriptionsService != null) { await subscriptionsService.OnInvalidUnsubscribeIntentVerificationAsync(subscription, subscriptionsStore); } } else if ((subscriptionsService == null) || (await subscriptionsService.OnUnsubscribeIntentVerificationAsync(subscription, subscriptionsStore))) { subscription.State = WebSubSubscriptionState.UnsubscribeValidated; subscription.VerificationRequestTimeStampUtc = DateTime.UtcNow; await subscriptionsStore.UpdateAsync(subscription); verified = true; } } return(verified); }
private async Task <IActionResult> HandleUnsubscribeIntentVerificationAsync(WebSubSubscription subscription, IWebSubSubscriptionsStore subscriptionsStore, IWebSubSubscriptionsService subscriptionsService, IQueryCollection requestQuery) { StringValues topicValues = requestQuery[WebSubConstants.TOPIC_QUERY_PARAMETER_NAME]; if (StringValues.IsNullOrEmpty(topicValues)) { return(HandleMissingIntentVerificationParameter(WebSubConstants.TOPIC_QUERY_PARAMETER_NAME)); } StringValues challengeValues = requestQuery[WebSubConstants.INTENT_VERIFICATION_CHALLENGE_QUERY_PARAMETER_NAME]; if (StringValues.IsNullOrEmpty(challengeValues)) { return(HandleMissingIntentVerificationParameter(WebSubConstants.INTENT_VERIFICATION_CHALLENGE_QUERY_PARAMETER_NAME)); } if (await VerifyUnsubscribeIntentAsync(subscription, subscriptionsStore, subscriptionsService, topicValues)) { _logger.LogInformation("Received an unsubscribe intent verification request for the '{ReceiverName}' WebHook receiver -- verification passed, returning challenge response.", WebSubConstants.ReceiverName); return(new ContentResult { Content = challengeValues }); } else { _logger.LogInformation("Received an unsubscribe intent verification request for the '{ReceiverName}' WebHook receiver -- verification failed, returning challenge response.", WebSubConstants.ReceiverName); return(new NotFoundResult()); } }
public Task RemoveAsync(WebSubSubscription subscription, CancellationToken cancellationToken) { if (subscription != null) { _store.TryRemove(subscription.Id, out _); } return(Task.CompletedTask); }
private WebSubRequestServices PrepareWebSubRequestServices(WebSubSubscription subscription = null) { subscription = subscription ?? PrepareWebSubSubscription(); return(new WebSubRequestServices(new Dictionary <string, WebSubSubscription> { { subscription.Id, subscription } })); }
private async Task <HttpResponseMessage> VerifyContentDistribution(WebSubSubscription subscription, HttpRequestMessage request) { if (subscription.State != WebSubSubscriptionState.SubscribeValidated) { return(request.CreateResponse(HttpStatusCode.NotFound)); } if (String.IsNullOrWhiteSpace(subscription.Secret)) { return(null); } string signatureHeader; try { signatureHeader = GetRequestHeader(request, SIGNATURE_HEADER_NAME); } catch (HttpResponseException) { return(HandleInvalidSignatureHeader(request)); } string[] tokens = signatureHeader.SplitAndTrim('='); if (tokens.Length != 2) { return(HandleInvalidSignatureHeader(request)); } byte[] signatureHeaderExpectedHash; try { signatureHeaderExpectedHash = EncodingUtilities.FromHex(tokens[1]); } catch (Exception) { return(HandleBadRequest(request, $"The '{SIGNATURE_HEADER_NAME}' header value is invalid. The '{RECEIVER_NAME}' WebHook receiver requires a valid hex-encoded string.")); } byte[] payloadActualHash = await ComputeRequestBodyHashAsync(request, tokens[0], Encoding.UTF8.GetBytes(subscription.Secret)); if (payloadActualHash == null) { return(HandleInvalidSignatureHeader(request)); } if (!SecretEqual(signatureHeaderExpectedHash, payloadActualHash)) { return(HandleBadRequest(request, $"The signature provided by the '{SIGNATURE_HEADER_NAME}' header field does not match the value expected by the '{RECEIVER_NAME}' WebHook receiver. WebHook request is invalid.")); } return(null); }
private async Task <IActionResult> VerifyContentDistributionRequest(WebSubSubscription subscription, HttpRequest request) { if (subscription.State != WebSubSubscriptionState.SubscribeValidated) { return(new NotFoundResult()); } if (String.IsNullOrWhiteSpace(subscription.Secret)) { return(null); } string signatureHeader = GetRequestHeader(request, WebSubConstants.SIGNATURE_HEADER_NAME, out IActionResult verificationResult); if (verificationResult != null) { return(verificationResult); } TrimmingTokenizer tokens = new TrimmingTokenizer(signatureHeader, _pairSeparators); if (tokens.Count != 2) { return(HandleInvalidSignatureHeader()); } TrimmingTokenizer.Enumerator tokensEnumerator = tokens.GetEnumerator(); tokensEnumerator.MoveNext(); StringSegment signatureHeaderKey = tokensEnumerator.Current; tokensEnumerator.MoveNext(); byte[] signatureHeaderExpectedHash = FromHex(tokensEnumerator.Current.Value, WebSubConstants.SIGNATURE_HEADER_NAME); if (signatureHeaderExpectedHash == null) { return(CreateBadHexEncodingResult(WebSubConstants.SIGNATURE_HEADER_NAME)); } byte[] payloadActualHash = await ComputeRequestBodyHashAsync(request, signatureHeaderKey, Encoding.UTF8.GetBytes(subscription.Secret)); if (payloadActualHash == null) { return(HandleInvalidSignatureHeader()); } if (!SecretEqual(signatureHeaderExpectedHash, payloadActualHash)) { return(CreateBadSignatureResult(WebSubConstants.SIGNATURE_HEADER_NAME)); } return(null); }
/// <summary> /// Creates new, initialized and already stored <see cref="WebSubSubscription"/>. /// </summary> /// <param name="cancellationToken">The cancellation token to cancel operation.</param> /// <returns>The task object representing the asynchronous operation.</returns> public async Task <WebSubSubscription> CreateAsync(CancellationToken cancellationToken) { WebSubSubscription subscription = new WebSubSubscription { Id = Guid.NewGuid().ToString("D"), State = WebSubSubscriptionState.Created }; _webSubDbContext.Subscriptions.Add(subscription); await _webSubDbContext.SaveChangesAsync(cancellationToken); return(subscription); }
public Task <WebSubSubscription> CreateAsync(CancellationToken cancellationToken) { WebSubSubscription subscription = new WebSubSubscription { Id = Guid.NewGuid().ToString("D"), State = WebSubSubscriptionState.Created }; if (!_store.TryAdd(subscription.Id, subscription)) { throw new Exception("Error while creating subscription"); } return(Task.FromResult(subscription)); }
/// <summary> /// Processes the incoming WebHook request. The request may be an initialization request or it may be /// an actual WebHook request. It is up to the receiver to determine what kind of incoming request it /// is and process it accordingly. /// </summary> /// <param name="id">A (potentially empty) ID of a particular configuration for this <see cref="IWebHookReceiver"/>. This /// allows an <see cref="IWebHookReceiver"/> to support multiple WebHooks with individual configurations.</param> /// <param name="context">The <see cref="HttpRequestContext"/> for the incoming request.</param> /// <param name="request">The <see cref="HttpRequestMessage"/> containing the incoming WebHook.</param> public override async Task <HttpResponseMessage> ReceiveAsync(string id, HttpRequestContext context, HttpRequestMessage request) { if (id == null) { throw new ArgumentNullException(nameof(id)); } if (context == null) { throw new ArgumentNullException(nameof(context)); } if (request == null) { throw new ArgumentNullException(nameof(request)); } WebSubSubscription subscription = await request.GetWebSubSubscriptionsStore().RetrieveAsync(id); if (subscription != null) { if (request.Method == HttpMethod.Get) { return(await HandleIntentVerificationAsync(subscription, request)); } else if (request.Method == HttpMethod.Post) { HttpResponseMessage contentDistributionVerificationResponse = await VerifyContentDistribution(subscription, request); if (contentDistributionVerificationResponse != null) { return(contentDistributionVerificationResponse); } return(await ExecuteWebHookAsync(id, context, request, Enumerable.Empty <string>(), new WebSubContent(request))); } else { return(CreateBadMethodResponse(request)); } } else { return(request.CreateResponse(HttpStatusCode.NotFound)); } }
private ResourceExecutingContext PrepareIntentVerificatioResourceExecutingContext( string mode = INTENT_VERIFICATION_MODE_SUBSCRIBE, string topic = WEBSUB_ROCKS_TOPIC_URL, string challenge = WEBSUB_ROCKS_CHALLENGE, string leaseSeconds = WEBSUB_ROCKS_LEASE_SECONDS, string reason = null, WebSubSubscription subscription = null, IServiceProvider requestServices = null) { subscription = subscription ?? PrepareWebSubSubscription(); return(new ResourceExecutingContext(new ActionContext() { ActionDescriptor = new ActionDescriptor(), HttpContext = new IntentVerificationHttpContext(mode, topic, challenge, leaseSeconds, reason, subscription, requestServices ?? PrepareWebSubRequestServices(subscription)), RouteData = new RouteData() }, new List <IFilterMetadata>(), new List <IValueProviderFactory>())); }
public async Task <SubscriptionViewModel> Subscribe(SubscribeViewModel subscribeViewModel, WebSubSubscriber webSubSubscriber) { if (!ModelState.IsValid) { return(new SubscriptionViewModel(ModelState)); } WebSubSubscription webSubSubscription = null; try { webSubSubscription = await _webSubSubscriptionsStore.CreateAsync(); webSubSubscription.CallbackUrl = Request.GetWebSubWebHookUrl(webSubSubscription.Id); WebSubSubscribeParameters webSubSubscribeParameters = new WebSubSubscribeParameters(subscribeViewModel.Url, webSubSubscription.CallbackUrl) { OnDiscoveredAsync = async(WebSubDiscoveredUrls discovery, CancellationToken cancellationToken) => { webSubSubscription.State = WebSubSubscriptionState.SubscribeRequested; webSubSubscription.TopicUrl = discovery.Topic; await _webSubSubscriptionsStore.UpdateAsync(webSubSubscription); } }; if (!String.IsNullOrWhiteSpace(subscribeViewModel.Secret)) { webSubSubscription.Secret = subscribeViewModel.Secret; webSubSubscribeParameters.Secret = subscribeViewModel.Secret; } webSubSubscription.HubUrl = (await webSubSubscriber.SubscribeAsync(webSubSubscribeParameters, HttpContext.RequestAborted)).Hub; return(new SubscriptionViewModel(webSubSubscription)); } catch (Exception ex) when((ex is WebSubDiscoveryException) || (ex is WebSubSubscriptionException)) { await _webSubSubscriptionsStore.RemoveAsync(webSubSubscription); return(new SubscriptionViewModel(ex)); } }
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { if (context == null) { throw new ArgumentNullException(nameof(context)); } IActionResult secureConnectionCheckResult = EnsureSecureConnection(ReceiverName, context.HttpContext.Request); if (secureConnectionCheckResult != null) { context.Result = secureConnectionCheckResult; return; } if (!context.RouteData.TryGetWebHookReceiverId(out string subscriptionId)) { context.Result = new NotFoundResult(); return; } WebSubSubscription subscription = await RetrieveWebSubSubscriptionAsync(context, subscriptionId); if (subscription == null) { context.Result = new NotFoundResult(); return; } if (HttpMethods.IsPost(context.HttpContext.Request.Method)) { IActionResult contentDistributionRequestVerificationResult = await VerifyContentDistributionRequest(subscription, context.HttpContext.Request); if (contentDistributionRequestVerificationResult != null) { context.Result = contentDistributionRequestVerificationResult; return; } } context.HttpContext.Items[WebSubConstants.HTTP_CONTEXT_ITEMS_SUBSCRIPTION_KEY] = subscription; await next(); }
/// <summary> /// Called asynchronously before the rest of the pipeline. /// </summary> /// <param name="context">The <see cref="ResourceExecutingContext"/>.</param> /// <param name="next">The <see cref="ResourceExecutionDelegate"/>. Invoked to execute the next resource filter or the remainder of the pipeline.</param> /// <returns>A <see cref="Task"/> which will complete when the remainder of the pipeline completes.</returns> public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { if (context == null) { throw new ArgumentNullException(nameof(context)); } IQueryCollection requestQuery = context.HttpContext.Request.Query; if (HttpMethods.IsGet(context.HttpContext.Request.Method) && requestQuery.ContainsKey(WebSubConstants.MODE_QUERY_PARAMETER_NAME)) { WebSubSubscription subscription = context.HttpContext.Items[WebSubConstants.HTTP_CONTEXT_ITEMS_SUBSCRIPTION_KEY] as WebSubSubscription; context.Result = await HandleIntentVerificationAsync(subscription, requestQuery, context.HttpContext.RequestServices); } else { await next(); } }
private async Task <IActionResult> HandleSubscribeIntentDenyAsync(WebSubSubscription subscription, IWebSubSubscriptionsStore subscriptionsStore, IWebSubSubscriptionsService subscriptionsService, IQueryCollection requestQuery) { StringValues topicValues = requestQuery[WebSubConstants.TOPIC_QUERY_PARAMETER_NAME]; if (StringValues.IsNullOrEmpty(topicValues)) { return(HandleBadRequest($"A '{WebSubConstants.ReceiverName}' WebHook subscribe intent deny request must contain a '{WebSubConstants.TOPIC_QUERY_PARAMETER_NAME}' query parameter.")); } StringValues reason = requestQuery[WebSubConstants.INTENT_DENY_REASON_QUERY_PARAMETER_NAME]; subscription.State = WebSubSubscriptionState.SubscribeDenied; await subscriptionsStore.UpdateAsync(subscription); if (subscriptionsService != null) { await subscriptionsService.OnSubscribeIntentDenyAsync(subscription, reason, subscriptionsStore); } _logger.LogInformation("Received a subscribe intent deny request for the '{ReceiverName}' WebHook receiver -- subscription denied, returning confirmation response.", WebSubConstants.ReceiverName); return(new NoContentResult()); }
public async Task <IActionResult> HandlerForContentDistribution(string id, WebSubSubscription subscription, IWebSubContent content) { await _serverSentEventsService.SendEventAsync($"HandlerForContentDistribution ({id})"); return(Ok()); }
public Task UpdateAsync(WebSubSubscription subscription) { return(UpdateAsync(subscription, CancellationToken.None)); }
public Task UpdateAsync(WebSubSubscription webSubSubscription, CancellationToken cancellationToken) { return(Task.CompletedTask); }
public Task RemoveAsync(WebSubSubscription subscription) { return(RemoveAsync(subscription, CancellationToken.None)); }
/// <summary> /// Performs store update of previously retrieved and updated <see cref="WebSubSubscription"/>. /// </summary> /// <param name="subscription">The <see cref="WebSubSubscription"/> to be updated in store.</param> /// <param name="cancellationToken">The cancellation token to cancel operation.</param> /// <returns>The task object representing the asynchronous operation.</returns> public Task UpdateAsync(WebSubSubscription subscription, CancellationToken cancellationToken) { return(_webSubDbContext.SaveChangesAsync(cancellationToken)); }
/// <summary> /// Removes from store <see cref="WebSubSubscription"/> based on its unique identifier. /// </summary> /// <param name="id">The unique identifier of the subscription.</param> /// <param name="cancellationToken">The cancellation token to cancel operation.</param> /// <returns>The task object representing the asynchronous operation.</returns> public async Task RemoveAsync(string id, CancellationToken cancellationToken) { WebSubSubscription subscription = await RetrieveAsync(id, cancellationToken); await RemoveAsync(subscription, cancellationToken); }
public SubscriptionViewModel(WebSubSubscription subscription) { Subscribed = true; Subscription = subscription; }
public Task UpdateAsync(WebSubSubscription subscription, CancellationToken cancellationToken) { return(Task.FromResult <Object>(null)); }
public Task <WebSubSubscription> RetrieveAsync(string id, CancellationToken cancellationToken) { WebSubSubscription subscription = _store.ContainsKey(id) ? _store[id] : null; return(Task.FromResult(subscription)); }
/// <summary> /// Removes from store previously retrieved <see cref="WebSubSubscription"/>. /// </summary> /// <param name="subscription">The <see cref="WebSubSubscription"/> to be removed from store.</param> /// <param name="cancellationToken">The cancellation token to cancel operation.</param> /// <returns>The task object representing the asynchronous operation.</returns> public Task RemoveAsync(WebSubSubscription subscription, CancellationToken cancellationToken) { _webSubDbContext.Subscriptions.Remove(subscription); return(_webSubDbContext.SaveChangesAsync(cancellationToken)); }
private static async Task <bool> VerifySubscribeIntentAsync(HttpRequestMessage request, WebSubSubscription subscription, string topic, int leaseSeconds) { bool verified = false; if (subscription.State == WebSubSubscriptionState.SubscribeRequested) { IWebSubSubscriptionsStore subscriptionsStore = request.GetWebSubSubscriptionsStore(); IWebSubSubscriptionsService subscriptionsService = request.GetWebSubSubscriptionsService(); if (subscription.TopicUrl != topic) { if (subscriptionsService != null) { await subscriptionsService.OnInvalidSubscribeIntentVerificationAsync(subscription, subscriptionsStore); } } else if ((subscriptionsService == null) || (await subscriptionsService.OnSubscribeIntentVerificationAsync(subscription, subscriptionsStore))) { subscription.State = WebSubSubscriptionState.SubscribeValidated; subscription.VerificationRequestTimeStampUtc = DateTime.UtcNow; subscription.LeaseSeconds = leaseSeconds; await subscriptionsStore.UpdateAsync(subscription); verified = true; } } return(verified); }
public Task RemoveAsync(WebSubSubscription subscription, CancellationToken cancellationToken) { throw new NotImplementedException(); }
public Task RemoveAsync(WebSubSubscription subscription) { throw new NotImplementedException(); }
private static async Task <HttpResponseMessage> HandleUnsubscribeIntentVerificationAsync(HttpRequestMessage request, Dictionary <string, string> requestQuery, WebSubSubscription subscription) { if (!requestQuery.TryGetValue(TOPIC_QUERY_PARAMETER_NAME, out string topic) || String.IsNullOrEmpty(topic)) { return(HandleMissingIntentVerificationParameter(request, TOPIC_QUERY_PARAMETER_NAME)); } if (!requestQuery.TryGetValue(INTENT_VERIFICATION_CHALLENGE_QUERY_PARAMETER_NAME, out string challenge) || String.IsNullOrEmpty(challenge)) { return(HandleMissingIntentVerificationParameter(request, INTENT_VERIFICATION_CHALLENGE_QUERY_PARAMETER_NAME)); } if (await VerifyUnsubscribeIntentAsync(request, subscription, topic)) { request.GetWebHooksLogger().Info($"Received an unsubscribe intent verification request for the '{ReceiverName}' WebHook receiver -- verification passed, returning challenge response."); return(CreateChallengeResponse(challenge)); } else { request.GetWebHooksLogger().Info($"Received an unsubscribe intent verification request for the '{ReceiverName}' WebHook receiver -- verification failed, returning challenge response."); return(request.CreateResponse(HttpStatusCode.NotFound)); } }