Skip to content

Commit 690b54e

Browse files
author
Henrik Frystyk Nielsen
committed
Added extra validation of WebHook instances during registration. As not all instances come in through the ASP.NET Web API registration controller, we have to run validation explicitly to verify that we have a correct WebHook registration.
To validate a WebHook instance, call IWebHookManager.VerifyWebHookAsync which will also verify that the registered WebHook URI is valid.
1 parent b779b4b commit 690b54e

File tree

8 files changed

+134
-3
lines changed

8 files changed

+134
-3
lines changed

src/Microsoft.AspNet.WebHooks.Custom/Properties/CustomResources.Designer.cs

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.AspNet.WebHooks.Custom/Properties/CustomResources.resx

+3
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@
159159
<data name="Notification_KeyNotFound" xml:space="preserve">
160160
<value>No Notification setting was found with key '{0}'.</value>
161161
</data>
162+
<data name="Sender_BadWorkItem" xml:space="preserve">
163+
<value>Invalid '{0}' instance: '{1}' cannot be null.</value>
164+
</data>
162165
<data name="WebHook_InvalidSecret" xml:space="preserve">
163166
<value>The WebHook secret key parameter must be between 32 and 64 characters long.</value>
164167
</data>

src/Microsoft.AspNet.WebHooks.Custom/WebHooks/WebHookManager.cs

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.ComponentModel.DataAnnotations;
67
using System.Globalization;
78
using System.Linq;
89
using System.Net.Http;
@@ -72,6 +73,10 @@ public async Task VerifyWebHookAsync(WebHook webHook)
7273
throw new ArgumentNullException("webHook");
7374
}
7475

76+
// Validate data annotations explicitly as the WebHook may not have gone through deserialization
77+
ValidationContext context = new ValidationContext(webHook);
78+
Validator.ValidateObject(webHook, context, validateAllProperties: true);
79+
7580
// Check that WebHook URI is either 'http' or 'https'
7681
if (!(webHook.WebHookUri.IsHttp() || webHook.WebHookUri.IsHttps()))
7782
{

src/Microsoft.AspNet.WebHooks.Custom/WebHooks/WebHookSender.cs

+7-2
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,19 @@ protected virtual JObject CreateWebHookRequestBody(WebHookWorkItem workItem)
154154
/// HTTP header to the <see cref="HttpRequestMessage"/> along with the entity body.
155155
/// </summary>
156156
/// <param name="workItem">The current <see cref="WebHookWorkItem"/>.</param>
157-
/// <param name="request">The request to add t</param>
158-
/// <param name="body">The body to sign and add to the request</param>
157+
/// <param name="request">The request to add the signature to.</param>
158+
/// <param name="body">The body to sign and add to the request.</param>
159159
protected virtual void SignWebHookRequest(WebHookWorkItem workItem, HttpRequestMessage request, JObject body)
160160
{
161161
if (workItem == null)
162162
{
163163
throw new ArgumentNullException("workItem");
164164
}
165+
if (workItem.WebHook == null)
166+
{
167+
string msg = string.Format(CultureInfo.CurrentCulture, CustomResources.Sender_BadWorkItem, this.GetType().Name, "WebHook");
168+
throw new ArgumentException(msg, "workItem");
169+
}
165170
if (request == null)
166171
{
167172
throw new ArgumentNullException("request");

test/Microsoft.AspNet.WebHooks.Custom.Test/Microsoft.AspNet.WebHooks.Custom.Test.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<Private>True</Private>
3030
</Reference>
3131
<Reference Include="System" />
32+
<Reference Include="System.ComponentModel.DataAnnotations" />
3233
<Reference Include="System.Core" />
3334
<Reference Include="System.Net.Http" />
3435
<Reference Include="System.Net.Http.Formatting, Version=5.2.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">

test/Microsoft.AspNet.WebHooks.Custom.Test/WebHooks/WebHookManagerTests.cs

+34-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Collections.ObjectModel;
77
using System.Collections.Specialized;
8+
using System.ComponentModel.DataAnnotations;
89
using System.Globalization;
910
using System.Linq;
1011
using System.Net;
@@ -43,6 +44,24 @@ public WebHookManagerTests()
4344
_response = new HttpResponseMessage();
4445
}
4546

47+
public static TheoryData<WebHook, string> WebHookValidationData
48+
{
49+
get
50+
{
51+
Uri webHookUri = new Uri("http://localhost");
52+
string secret = new string('a', 32);
53+
return new TheoryData<WebHook, string>
54+
{
55+
{ new WebHook(), "The WebHookUri field is required." },
56+
{ new WebHook { WebHookUri = null, Secret = secret }, "The WebHookUri field is required." },
57+
{ new WebHook { WebHookUri = webHookUri, Secret = null }, "The Secret field is required." },
58+
{ new WebHook { WebHookUri = webHookUri, Secret = string.Empty }, "The Secret field is required." },
59+
{ new WebHook { WebHookUri = webHookUri, Secret = "a" }, "The WebHook secret key parameter must be between 32 and 64 characters long." },
60+
{ new WebHook { WebHookUri = webHookUri, Secret = new string('a', 65) }, "The WebHook secret key parameter must be between 32 and 64 characters long." },
61+
};
62+
}
63+
}
64+
4665
public static TheoryData<IEnumerable<WebHook>, NotificationDictionary> FilterSingleNotificationData
4766
{
4867
get
@@ -84,6 +103,20 @@ public static TheoryData<IEnumerable<WebHook>, IEnumerable<NotificationDictionar
84103
}
85104
}
86105

106+
[Theory]
107+
[MemberData("WebHookValidationData")]
108+
public async Task VerifyWebHookAsync_Throws_IfInvalidWebHook(WebHook webHook, string expected)
109+
{
110+
// Arrange
111+
_manager = new WebHookManager(_storeMock.Object, _senderMock.Object, _loggerMock.Object, _httpClient);
112+
113+
// Act
114+
ValidationException ex = await Assert.ThrowsAsync<ValidationException>(() => _manager.VerifyWebHookAsync(webHook));
115+
116+
// Assert
117+
Assert.Equal(expected, ex.Message);
118+
}
119+
87120
[Theory]
88121
[InlineData("ftp://localhost")]
89122
[InlineData("telnet://localhost")]
@@ -94,7 +127,7 @@ public async Task VerifyWebHookAsync_Throws_IfNotHttpOrHttpsUri(string webHookUr
94127
// Arrange
95128
_manager = new WebHookManager(_storeMock.Object, _senderMock.Object, _loggerMock.Object, _httpClient);
96129
WebHook webHook = CreateWebHook();
97-
webHook.WebHookUri = new Uri(webHookUri);
130+
webHook.WebHookUri = webHookUri != null ? new Uri(webHookUri) : null;
98131

99132
// Act
100133
InvalidOperationException ex = await Assert.ThrowsAsync<InvalidOperationException>(() => _manager.VerifyWebHookAsync(webHook));

test/Microsoft.AspNet.WebHooks.Custom.Test/WebHooks/WebHookSenderTests.cs

+16
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,22 @@ public void CreateWebHookRequestBody_CreatesExpectedBody()
6868
Assert.Equal(SerializedWebHook, actual.ToString());
6969
}
7070

71+
[Fact]
72+
public void SignWebHookRequest_HandlesNullWebHook()
73+
{
74+
WebHookWorkItem workItem = CreateWorkItem();
75+
HttpRequestMessage request = new HttpRequestMessage();
76+
_sender = new WebHookSenderMock(_loggerMock.Object);
77+
JObject body = _sender.CreateWebHookRequestBody(workItem);
78+
workItem.WebHook = null;
79+
80+
// Act
81+
ArgumentException ex = Assert.Throws<ArgumentException>(() => _sender.SignWebHookRequest(workItem, request, body));
82+
83+
// Assert
84+
Assert.StartsWith("Invalid 'WebHookSenderMock' instance: 'WebHook' cannot be null.", ex.Message);
85+
}
86+
7187
[Fact]
7288
public async Task SignWebHookRequest_SignsBodyCorrectly()
7389
{

test/Microsoft.AspNet.WebHooks.Custom.Test/WebHooks/WebHookTests.cs

+59
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.ComponentModel.DataAnnotations;
7+
using System.Linq;
68
using Microsoft.TestUtilities;
79
using Newtonsoft.Json;
810
using Xunit;
@@ -19,6 +21,31 @@ public WebHookTests()
1921
_webHook = new WebHook();
2022
}
2123

24+
public enum ValidationOutcome
25+
{
26+
Valid = 0,
27+
Required,
28+
Invalid
29+
}
30+
31+
public static TheoryData<string, ValidationOutcome> WebHookSecretData
32+
{
33+
get
34+
{
35+
return new TheoryData<string, ValidationOutcome>
36+
{
37+
{ string.Empty, ValidationOutcome.Required },
38+
{ " ", ValidationOutcome.Required },
39+
{ "\r\n", ValidationOutcome.Required },
40+
{ new string('a', 31), ValidationOutcome.Invalid },
41+
{ new string('a', 65), ValidationOutcome.Invalid },
42+
{ new string('a', 32), ValidationOutcome.Valid },
43+
{ new string('a', 64), ValidationOutcome.Valid },
44+
{ "你好世界你好世界你好世界你好世界你好世界你好世界你好世界你好世界", ValidationOutcome.Valid },
45+
};
46+
}
47+
}
48+
2249
[Fact]
2350
public void Id_Roundtrips()
2451
{
@@ -38,6 +65,38 @@ public void Secret_Roundtrips()
3865
PropertyAssert.Roundtrips(_webHook, w => w.Secret, PropertySetter.NullRoundtrips, roundtripValue: "你好世界");
3966
}
4067

68+
[Theory]
69+
[MemberData("WebHookSecretData")]
70+
public void Secret_Validates(string secret, ValidationOutcome expected)
71+
{
72+
// Arrange
73+
WebHook webHook = new WebHook { Secret = secret };
74+
var validationResults = new List<ValidationResult>();
75+
var context = new ValidationContext(webHook) { MemberName = "Secret" };
76+
77+
// Act
78+
bool actual = Validator.TryValidateProperty(webHook.Secret, context, validationResults);
79+
80+
// Assert
81+
switch (expected)
82+
{
83+
case ValidationOutcome.Valid:
84+
Assert.True(actual);
85+
break;
86+
87+
case ValidationOutcome.Required:
88+
Assert.False(actual);
89+
Assert.Equal("The Secret field is required.", validationResults.Single().ErrorMessage);
90+
Assert.Equal("Secret", validationResults.Single().MemberNames.Single());
91+
break;
92+
93+
case ValidationOutcome.Invalid:
94+
Assert.Equal("The WebHook secret key parameter must be between 32 and 64 characters long.", validationResults.Single().ErrorMessage);
95+
Assert.Equal("Secret", validationResults.Single().MemberNames.Single());
96+
break;
97+
}
98+
}
99+
41100
[Fact]
42101
public void Description_Roundtrips()
43102
{

0 commit comments

Comments
 (0)