Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Common/HmacDigest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;

namespace FHIRcastSandbox.Rules {
public class HmacDigest {
public string CreateDigest(string key, string payload) {
var byteKey = System.Text.Encoding.UTF8.GetBytes(key);
using (var hmacHasher = new System.Security.Cryptography.HMACSHA256(byteKey)) {
var digest = hmacHasher.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payload));
return BitConverter.ToString(digest).Replace("-", "").ToLower();
}
}

public string CreateHubSignature(string key, string payload) {
return $"sha256={this.CreateDigest(key, payload)}";
}

public bool VerifyHubSignature(string key, string payload, string signature) {
return false;
/* return this.CreateDigest(key, payload) == signature; */
}
}
}
18 changes: 17 additions & 1 deletion Common/Model/HttpSubscription.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Net.Http.Headers;
using System.Net.Http;
using FHIRcastSandbox.Model;
using FHIRcastSandbox.Rules;

namespace FHIRcastSandbox.Model.Http {
public static class SubscriptionExtensions {
Expand All @@ -20,5 +22,19 @@ public static HttpContent CreateHttpContent(this Subscription source) {
return httpcontent;
}
}
}

public static class NotificationExtensions {
public static HttpContent CreateHttpContent(this Notification source, string subscriptionSecret = null) {
var str = Newtonsoft.Json.JsonConvert.SerializeObject(source);
var content = new StringContent(str);
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

if (subscriptionSecret != null) {
var hubSignature = new HmacDigest().CreateHubSignature(subscriptionSecret, str);
content.Headers.Add("X-Hub-Signature", hubSignature);
}

return content;
}
}
}
24 changes: 24 additions & 0 deletions Common/Model/Subscriptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,30 @@ public class Notification : ModelBase {
public string Id { get; set; }
[JsonProperty(PropertyName = "event")]
public NotificationEvent Event { get; } = new NotificationEvent();

public Notification() {
this.Id = Guid.NewGuid().ToString();
this.Timestamp = DateTime.Now;
}

public override bool Equals(object other) {
var notification = other as Notification;
return notification != null &&
this.Timestamp == notification.Timestamp &&
this.Id == notification.Id &&
this.Event.Topic == notification.Event.Topic &&
this.Event.Event == notification.Event.Event;

}

public override int GetHashCode() {
return new {
this.Timestamp,
this.Id,
this.Event.Topic,
this.Event.Event,
}.GetHashCode();
}
}

public class NotificationEvent {
Expand Down
9 changes: 4 additions & 5 deletions Hub/Rules/Notifications.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using FHIRcastSandbox.Model;
using Microsoft.Extensions.Logging;
using System.Net.Http.Headers;
using System.Net.Http;
using System.Threading.Tasks;
using System;
using FHIRcastSandbox.Model.Http;
using FHIRcastSandbox.Model;
using Microsoft.Extensions.Logging;

namespace FHIRcastSandbox.Rules {
public class Notifications : INotifications {
Expand All @@ -16,10 +17,8 @@ public Notifications(ILogger<Notifications> logger) {
public async Task SendNotification(Notification notification, Subscription subscription) {
this.logger.LogInformation($"Sending notification {notification} to callback {subscription.Callback}");

var str = Newtonsoft.Json.JsonConvert.SerializeObject(notification);
var content = new StringContent(str);
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var client = new HttpClient();
var content = notification.CreateHttpContent(subscription.Secret);
var response = await client.PostAsync(subscription.Callback, content);

this.logger.LogDebug($"Got response from posting notification:{Environment.NewLine}{response}{Environment.NewLine}{await response.Content.ReadAsStringAsync()}.");
Expand Down
42 changes: 42 additions & 0 deletions Tests/HmacDigestTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using Xunit;

namespace FHIRcastSandbox.Rules {
public class HmacDigestTests {
[Fact]
public void CreateDigest_KeyAndPayload_CreateCorrectDigest_Test() {
// Arange
var key = "key";
var payload = "The quick brown fox jumps over the lazy dog";

// Act
var result = new HmacDigest().CreateDigest(key, payload);

// Assert
Assert.Equal("f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8", result);
}

[Fact]
public void CreateHubSignature_KeyAndPayload_CreateSignatureForSha256_Test() {
// Arange
var key = "key";
var payload = "The quick brown fox jumps over the lazy dog";

// Act
var result = new HmacDigest().CreateHubSignature(key, payload);

// Assert
Assert.Equal("sha256=f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8", result);
}

[Fact]
public void VerifyHubSignature_CorrectKeyAndPayload_ReturnsTrue_Test() {
// Arrange
var key = "key";
var payload = "The quick brown fox jumps over the lazy dog";

// Act
var result = new HmacDigest().VerifyHubSignature(key, payload, signature);
}
}
}
45 changes: 36 additions & 9 deletions Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
using System.Net.Http;
using System.Net.Sockets;
using System.Threading.Tasks;
using FHIRcastSandbox.Model;
using FHIRcastSandbox.Hubs;
using FHIRcastSandbox.Model.Http;
using FHIRcastSandbox.Model;
using FakeItEasy;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Xunit;

Expand All @@ -18,14 +23,15 @@ public class IntegrationTests : IDisposable {
private readonly IWebHost webSubClientServer;
private readonly int hubServerPort;
private readonly int webSubClientServerPort;
private IClientProxy signalrClientProxy;

public IntegrationTests() {
this.hubServerPort = this.GetFreePort();
this.hubServer = this.CreateHubServer(this.hubServerPort);
Console.WriteLine($"Hub: http://localhost:{this.hubServerPort}");

this.webSubClientServerPort = this.GetFreePort();
this.webSubClientServer = this.CreateWebSubClientServer(this.webSubClientServerPort);
(this.webSubClientServer, this.signalrClientProxy) = this.CreateWebSubClientServer(this.webSubClientServerPort);
Console.WriteLine($"WebSubClient: http://localhost:{this.webSubClientServerPort}");

Task.WaitAll(
Expand All @@ -34,9 +40,14 @@ public IntegrationTests() {
System.Threading.Thread.Sleep(1000);
}

private IWebHost CreateWebSubClientServer(int port) {
private (IWebHost, IClientProxy) CreateWebSubClientServer(int port) {
var testSignalrHubContext = A.Fake<IHubContext<WebSubClientHub>>();
var signalrClientProxy = A.Fake<IClientProxy>();
A.CallTo(() => testSignalrHubContext.Clients.Clients(A<string[]>._))
.Returns(signalrClientProxy);

var contentRoot = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "WebSubClient");
return FHIRcastSandbox.WebSubClient.Program.CreateWebHostBuilder()
var webHost = FHIRcastSandbox.WebSubClient.Program.CreateWebHostBuilder()
.UseKestrel()
.UseContentRoot(contentRoot)
.UseUrls($"http://localhost:{port}")
Expand All @@ -45,7 +56,12 @@ private IWebHost CreateWebSubClientServer(int port) {
{ "Settings:ValidateSubscriptionValidations", "False" },
{ "Logging:LogLevel:Default", "Warning" },
}))
.ConfigureTestServices(services => {
services.AddSingleton<IHubContext<WebSubClientHub>>(testSignalrHubContext);
})
.Build();

return (webHost, signalrClientProxy);
}

private IWebHost CreateHubServer(int port) {
Expand Down Expand Up @@ -76,24 +92,35 @@ public async Task ListingSubscriptions_AfterSubscribingToHub_ReturnsSubsription_
var connectionId = "some_client_connection_id";
var subscriptionUrl = $"http://localhost:{this.hubServerPort}/api/hub";
var topic = $"{subscriptionUrl}/{sessionId}";
var secret = "some_secret";
var events = new[] { "some_event" };
var callback = $"http://localhost:{this.webSubClientServerPort}/callback/{connectionId}";
var subscription = Subscription.CreateNewSubscription(subscriptionUrl, topic, events, callback);
subscription.Secret = secret;
var httpContent = subscription.CreateHttpContent();

var clientTestResponse = await new HttpClient().GetAsync(callback);
Assert.True(clientTestResponse.IsSuccessStatusCode, $"Could not connect to web sub client: {clientTestResponse}");

// Subscribe to Hub
var subscriptionResponse = await new HttpClient().PostAsync(subscriptionUrl, httpContent);
Assert.True(subscriptionResponse.IsSuccessStatusCode, $"Could not subscribe to hub: {subscriptionResponse}");
await Task.Delay(1000);

// Act
var result = await new HttpClient().GetStringAsync(subscriptionUrl);
var subscriptions = JsonConvert.DeserializeObject<Subscription[]>(result);
// Notify Hub
var notification = new Notification();
notification.Event.Topic = topic;
notification.Event.Event = events[0];
var notificationContent = notification.CreateHttpContent();
var notificationResult = await new HttpClient().PostAsync(topic, notificationContent);
notificationResult.EnsureSuccessStatusCode();

// Assert
Assert.Single(subscriptions);
// Assert that the notificaiton was sent to the web client
A.CallTo(() => this.signalrClientProxy.SendCoreAsync(
"notification",
A<object[]>.That.IsSameSequenceAs(new[] { notification }),
A<System.Threading.CancellationToken>._))
.MustHaveHappened();
}

public void Dispose() {
Expand Down
1 change: 1 addition & 0 deletions Tests/Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FakeItEasy" Version="4.9.1" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="2.1.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
Expand Down
14 changes: 3 additions & 11 deletions WebSubClient/Controllers/CallbackController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,10 @@ public IActionResult SubscriptionVerification(string connectionId, [FromQuery] S
/// <returns></returns>
[HttpPost("{subscriptionId}")]
public async Task<IActionResult> Notification(string subscriptionId, [FromBody] Notification notification) {
//If we do not have an active subscription matching the id then return a notfound error
var clients = this.clientSubscriptions.GetSubscribedClients(notification);
var subscription = this.clientSubscriptions.GetSubscription(subscriptionId);
var key = subscription.Secret;

//var internalModel = new ClientModel()
//{
// UserIdentifier = notification.Event.Context[0] == null ? "" : notification.Event.Context[0].ToString(),
// PatientIdentifier = notification.Event.Context[1] == null ? "" : notification.Event.Context[1].ToString(),
// PatientIdIssuer = notification.Event.Context[2] == null ? "" : notification.Event.Context[2].ToString(),
// AccessionNumber = notification.Event.Context[3] == null ? "" : notification.Event.Context[3].ToString(),
// AccessionNumberGroup = notification.Event.Context[4] == null ? "" : notification.Event.Context[4].ToString(),
// StudyId = notification.Event.Context[5] == null ? "" : notification.Event.Context[5].ToString(),
//};
var clients = this.clientSubscriptions.GetSubscribedClients(notification);

await this.webSubClientHubContext.Clients.Clients(clients)
.SendAsync("notification", notification);
Expand Down
10 changes: 4 additions & 6 deletions WebSubClient/Controllers/WebSubClientController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using FHIRcastSandbox.Model.Http;
using FHIRcastSandbox.Model;
using FHIRcastSandbox.WebSubClient.Rules;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -43,11 +44,7 @@ public WebSubClientController(ILogger<WebSubClientController> logger, ClientSubs
[HttpPost("notify")]
public async Task<IActionResult> Post([FromForm] ClientModel model) {
var httpClient = new HttpClient();
var notification = new Notification
{
Id = Guid.NewGuid().ToString(),
Timestamp = DateTime.Now,
};
var notification = new Notification();
notification.Event.Context = new[] {
new {
model.AccessionNumber,
Expand All @@ -61,7 +58,8 @@ public async Task<IActionResult> Post([FromForm] ClientModel model) {
notification.Event.Topic = model.Topic;
notification.Event.Event = model.Event;

var response = await httpClient.PostAsync(model.Topic, new StringContent(JsonConvert.SerializeObject(notification), Encoding.UTF8, "application/json"));
var content = notification.CreateHttpContent();
var response = await httpClient.PostAsync(model.Topic, content);

response.EnsureSuccessStatusCode();

Expand Down