Skip to content

Commit b9f1095

Browse files
author
clemensv
committed
HTTP WebHook validation
Signed-off-by: clemensv <[email protected]>
1 parent 2e88ade commit b9f1095

File tree

2 files changed

+160
-1
lines changed

2 files changed

+160
-1
lines changed

src/CloudNative.CloudEvents/HttpClientExtension.cs

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,127 @@ public static async Task CopyFromAsync(this HttpWebRequest httpWebRequest, Cloud
7777
await stream.CopyToAsync(httpWebRequest.GetRequestStream());
7878
}
7979

80+
/// <summary>
81+
/// Handle the request as WebHook validation request
82+
/// </summary>
83+
/// <param name="httpRequestMessage">Request</param>
84+
/// <param name="validateOrigin">Callback that returns whether the given origin may push events. If 'null', all origins are acceptable.</param>
85+
/// <param name="validateRate">Callback that returns the acceptable request rate. If 'null', the rate is not limited.</param>
86+
/// <returns>Response</returns>
87+
public static async Task<HttpResponseMessage> HandleAsWebHookValidationRequest(
88+
this HttpRequestMessage httpRequestMessage, Func<string, bool> validateOrigin,
89+
Func<string, string> validateRate)
90+
{
91+
if (IsWebHookValidationRequest(httpRequestMessage))
92+
{
93+
var origin = httpRequestMessage.Headers.GetValues("WebHook-Request-Origin").FirstOrDefault();
94+
var rate = httpRequestMessage.Headers.GetValues("WebHook-Request-Rate").FirstOrDefault();
95+
96+
if (origin != null && (validateOrigin == null || validateOrigin(origin)))
97+
{
98+
if (rate != null)
99+
{
100+
if (validateRate != null)
101+
{
102+
rate = validateRate(rate);
103+
}
104+
else
105+
{
106+
rate = "*";
107+
}
108+
}
109+
110+
if (httpRequestMessage.Headers.Contains("WebHook-Request-Callback"))
111+
{
112+
var uri = httpRequestMessage.Headers.GetValues("WebHook-Request-Callback").FirstOrDefault();
113+
try
114+
{
115+
HttpClient client = new HttpClient();
116+
var response = await client.GetAsync(new Uri(uri));
117+
return new HttpResponseMessage(response.StatusCode);
118+
}
119+
catch (Exception e)
120+
{
121+
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
122+
}
123+
}
124+
else
125+
{
126+
var response = new HttpResponseMessage(HttpStatusCode.OK);
127+
response.Headers.Add("Allow", "POST");
128+
response.Headers.Add("WebHook-Allowed-Origin", origin);
129+
response.Headers.Add("WebHook-Allowed-Rate", rate);
130+
return response;
131+
}
132+
}
133+
}
134+
135+
return new HttpResponseMessage(HttpStatusCode.MethodNotAllowed);
136+
}
137+
138+
/// <summary>
139+
/// Handle the request as WebHook validation request
140+
/// </summary>
141+
/// <param name="context">Request context</param>
142+
/// <param name="validateOrigin">Callback that returns whether the given origin may push events. If 'null', all origins are acceptable.</param>
143+
/// <param name="validateRate">Callback that returns the acceptable request rate. If 'null', the rate is not limited.</param>
144+
/// <returns>Task</returns>
145+
public static async Task HandleAsWebHookValidationRequest(this HttpListenerContext context,
146+
Func<string, bool> validateOrigin, Func<string, string> validateRate)
147+
{
148+
if (IsWebHookValidationRequest(context.Request))
149+
{
150+
var origin = context.Request.Headers.Get("WebHook-Request-Origin");
151+
var rate = context.Request.Headers.Get("WebHook-Request-Rate");
152+
153+
if (origin != null && (validateOrigin == null || validateOrigin(origin)))
154+
{
155+
if (rate != null)
156+
{
157+
if (validateRate != null)
158+
{
159+
rate = validateRate(rate);
160+
}
161+
else
162+
{
163+
rate = "*";
164+
}
165+
}
166+
167+
if (context.Request.Headers["WebHook-Request-Callback"] != null)
168+
{
169+
var uri = context.Request.Headers.Get("WebHook-Request-Callback");
170+
try
171+
{
172+
HttpClient client = new HttpClient();
173+
var response = await client.GetAsync(new Uri(uri));
174+
context.Response.StatusCode = (int)response.StatusCode;
175+
context.Response.Close();
176+
return;
177+
}
178+
catch (Exception e)
179+
{
180+
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
181+
context.Response.Close();
182+
return;
183+
}
184+
}
185+
else
186+
{
187+
context.Response.StatusCode = (int)HttpStatusCode.OK;
188+
context.Response.Headers.Add("Allow", "POST");
189+
context.Response.Headers.Add("WebHook-Allowed-Origin", origin);
190+
context.Response.Headers.Add("WebHook-Allowed-Rate", rate);
191+
context.Response.Close();
192+
return;
193+
}
194+
}
195+
}
196+
197+
context.Response.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
198+
context.Response.Close();
199+
}
200+
80201
/// <summary>
81202
/// Indicates whether this HttpResponseMessage holds a CloudEvent
82203
/// </summary>
@@ -101,6 +222,24 @@ public static bool IsCloudEvent(this HttpListenerRequest httpListenerRequest)
101222
httpListenerRequest.Headers[SpecVersionHttpHeader2] != null;
102223
}
103224

225+
/// <summary>
226+
/// Indicates whether this HttpListenerRequest is a web hook validation request
227+
/// </summary>
228+
public static bool IsWebHookValidationRequest(this HttpRequestMessage httpRequestMessage)
229+
{
230+
return (httpRequestMessage.Method.Method.Equals("options", StringComparison.InvariantCultureIgnoreCase) &&
231+
httpRequestMessage.Headers.Contains("WebHook-Request-Origin"));
232+
}
233+
234+
/// <summary>
235+
/// Indicates whether this HttpListenerRequest is a web hook validation request
236+
/// </summary>
237+
public static bool IsWebHookValidationRequest(this HttpListenerRequest httpRequestMessage)
238+
{
239+
return (httpRequestMessage.HttpMethod.Equals("options", StringComparison.InvariantCultureIgnoreCase) &&
240+
httpRequestMessage.Headers["WebHook-Request-Origin"] != null);
241+
}
242+
104243
/// <summary>
105244
/// Converts this response message into a CloudEvent object, with the given extensions.
106245
/// </summary>
@@ -382,7 +521,7 @@ static CloudEvent ToCloudEventInternal(HttpResponseMessage httpResponseMessage,
382521
else
383522
{
384523
attributes[name] = headerValue;
385-
}
524+
}
386525
}
387526
}
388527

test/CloudNative.CloudEvents.UnitTests/HttpTest.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace CloudNative.CloudEvents.UnitTests
77
using System;
88
using System.Collections.Concurrent;
99
using System.IO;
10+
using System.Linq;
1011
using System.Net;
1112
using System.Net.Http;
1213
using System.Net.Mime;
@@ -49,6 +50,13 @@ public void Dispose()
4950
async Task HandleContext(HttpListenerContext requestContext)
5051
{
5152
var ctxHeaderValue = requestContext.Request.Headers[testContextHeader];
53+
54+
if (requestContext.Request.IsWebHookValidationRequest())
55+
{
56+
await requestContext.HandleAsWebHookValidationRequest(null, null);
57+
return;
58+
}
59+
5260
if (pendingRequests.TryRemove(ctxHeaderValue, out var pending))
5361
{
5462
await pending(requestContext);
@@ -64,6 +72,18 @@ async Task HandleContext(HttpListenerContext requestContext)
6472
#pragma warning restore 4014
6573
}
6674

75+
[Fact]
76+
async Task HttpWebHookValidation()
77+
{
78+
var httpClient = new HttpClient();
79+
var req = new HttpRequestMessage(HttpMethod.Options, new Uri(listenerAddress + "ep"));
80+
req.Headers.Add("WebHook-Request-Origin", "example.com");
81+
req.Headers.Add("WebHook-Request-Rate", "120");
82+
var result = await httpClient.SendAsync( req );
83+
Assert.Equal("example.com", result.Headers.GetValues("WebHook-Allowed-Origin").First());
84+
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
85+
}
86+
6787
[Fact]
6888
async Task HttpBinaryClientReceiveTest()
6989
{

0 commit comments

Comments
 (0)