diff --git a/src/CloudNative.CloudEvents/HttpClientExtension.cs b/src/CloudNative.CloudEvents/HttpClientExtension.cs index 9864e35..ad52ad8 100644 --- a/src/CloudNative.CloudEvents/HttpClientExtension.cs +++ b/src/CloudNative.CloudEvents/HttpClientExtension.cs @@ -77,6 +77,127 @@ namespace CloudNative.CloudEvents await stream.CopyToAsync(httpWebRequest.GetRequestStream()); } + /// + /// Handle the request as WebHook validation request + /// + /// Request + /// Callback that returns whether the given origin may push events. If 'null', all origins are acceptable. + /// Callback that returns the acceptable request rate. If 'null', the rate is not limited. + /// Response + public static async Task HandleAsWebHookValidationRequest( + this HttpRequestMessage httpRequestMessage, Func validateOrigin, + Func validateRate) + { + if (IsWebHookValidationRequest(httpRequestMessage)) + { + var origin = httpRequestMessage.Headers.GetValues("WebHook-Request-Origin").FirstOrDefault(); + var rate = httpRequestMessage.Headers.GetValues("WebHook-Request-Rate").FirstOrDefault(); + + if (origin != null && (validateOrigin == null || validateOrigin(origin))) + { + if (rate != null) + { + if (validateRate != null) + { + rate = validateRate(rate); + } + else + { + rate = "*"; + } + } + + if (httpRequestMessage.Headers.Contains("WebHook-Request-Callback")) + { + var uri = httpRequestMessage.Headers.GetValues("WebHook-Request-Callback").FirstOrDefault(); + try + { + HttpClient client = new HttpClient(); + var response = await client.GetAsync(new Uri(uri)); + return new HttpResponseMessage(response.StatusCode); + } + catch (Exception e) + { + return new HttpResponseMessage(HttpStatusCode.InternalServerError); + } + } + else + { + var response = new HttpResponseMessage(HttpStatusCode.OK); + response.Headers.Add("Allow", "POST"); + response.Headers.Add("WebHook-Allowed-Origin", origin); + response.Headers.Add("WebHook-Allowed-Rate", rate); + return response; + } + } + } + + return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed); + } + + /// + /// Handle the request as WebHook validation request + /// + /// Request context + /// Callback that returns whether the given origin may push events. If 'null', all origins are acceptable. + /// Callback that returns the acceptable request rate. If 'null', the rate is not limited. + /// Task + public static async Task HandleAsWebHookValidationRequest(this HttpListenerContext context, + Func validateOrigin, Func validateRate) + { + if (IsWebHookValidationRequest(context.Request)) + { + var origin = context.Request.Headers.Get("WebHook-Request-Origin"); + var rate = context.Request.Headers.Get("WebHook-Request-Rate"); + + if (origin != null && (validateOrigin == null || validateOrigin(origin))) + { + if (rate != null) + { + if (validateRate != null) + { + rate = validateRate(rate); + } + else + { + rate = "*"; + } + } + + if (context.Request.Headers["WebHook-Request-Callback"] != null) + { + var uri = context.Request.Headers.Get("WebHook-Request-Callback"); + try + { + HttpClient client = new HttpClient(); + var response = await client.GetAsync(new Uri(uri)); + context.Response.StatusCode = (int)response.StatusCode; + context.Response.Close(); + return; + } + catch (Exception e) + { + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + context.Response.Close(); + return; + } + } + else + { + context.Response.StatusCode = (int)HttpStatusCode.OK; + context.Response.Headers.Add("Allow", "POST"); + context.Response.Headers.Add("WebHook-Allowed-Origin", origin); + context.Response.Headers.Add("WebHook-Allowed-Rate", rate); + context.Response.Close(); + return; + } + } + } + + context.Response.StatusCode = (int)HttpStatusCode.MethodNotAllowed; + context.Response.Close(); + } + /// /// Indicates whether this HttpResponseMessage holds a CloudEvent /// @@ -101,6 +222,24 @@ namespace CloudNative.CloudEvents httpListenerRequest.Headers[SpecVersionHttpHeader2] != null; } + /// + /// Indicates whether this HttpListenerRequest is a web hook validation request + /// + public static bool IsWebHookValidationRequest(this HttpRequestMessage httpRequestMessage) + { + return (httpRequestMessage.Method.Method.Equals("options", StringComparison.InvariantCultureIgnoreCase) && + httpRequestMessage.Headers.Contains("WebHook-Request-Origin")); + } + + /// + /// Indicates whether this HttpListenerRequest is a web hook validation request + /// + public static bool IsWebHookValidationRequest(this HttpListenerRequest httpRequestMessage) + { + return (httpRequestMessage.HttpMethod.Equals("options", StringComparison.InvariantCultureIgnoreCase) && + httpRequestMessage.Headers["WebHook-Request-Origin"] != null); + } + /// /// Converts this response message into a CloudEvent object, with the given extensions. /// @@ -382,7 +521,7 @@ namespace CloudNative.CloudEvents else { attributes[name] = headerValue; - } + } } } diff --git a/test/CloudNative.CloudEvents.UnitTests/HttpTest.cs b/test/CloudNative.CloudEvents.UnitTests/HttpTest.cs index 12d5bc0..d20cde6 100644 --- a/test/CloudNative.CloudEvents.UnitTests/HttpTest.cs +++ b/test/CloudNative.CloudEvents.UnitTests/HttpTest.cs @@ -7,6 +7,7 @@ namespace CloudNative.CloudEvents.UnitTests using System; using System.Collections.Concurrent; using System.IO; + using System.Linq; using System.Net; using System.Net.Http; using System.Net.Mime; @@ -49,6 +50,13 @@ namespace CloudNative.CloudEvents.UnitTests async Task HandleContext(HttpListenerContext requestContext) { var ctxHeaderValue = requestContext.Request.Headers[testContextHeader]; + + if (requestContext.Request.IsWebHookValidationRequest()) + { + await requestContext.HandleAsWebHookValidationRequest(null, null); + return; + } + if (pendingRequests.TryRemove(ctxHeaderValue, out var pending)) { await pending(requestContext); @@ -64,6 +72,18 @@ namespace CloudNative.CloudEvents.UnitTests #pragma warning restore 4014 } + [Fact] + async Task HttpWebHookValidation() + { + var httpClient = new HttpClient(); + var req = new HttpRequestMessage(HttpMethod.Options, new Uri(listenerAddress + "ep")); + req.Headers.Add("WebHook-Request-Origin", "example.com"); + req.Headers.Add("WebHook-Request-Rate", "120"); + var result = await httpClient.SendAsync( req ); + Assert.Equal("example.com", result.Headers.GetValues("WebHook-Allowed-Origin").First()); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + } + [Fact] async Task HttpBinaryClientReceiveTest() {